@fragments-sdk/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/bin.js +294 -50
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-D35RGPAG.js → chunk-7OPWMLOE.js} +435 -19
  4. package/dist/chunk-7OPWMLOE.js.map +1 -0
  5. package/dist/{chunk-SSLQXHNX.js → chunk-CVXKXVOY.js} +1 -1
  6. package/dist/{chunk-SSLQXHNX.js.map → chunk-CVXKXVOY.js.map} +1 -1
  7. package/dist/{chunk-Q7GOHVOK.js → chunk-TJ34N7C7.js} +39 -2
  8. package/dist/{chunk-Q7GOHVOK.js.map → chunk-TJ34N7C7.js.map} +1 -1
  9. package/dist/{chunk-F7ITZPDJ.js → chunk-XHUDJNN3.js} +2 -2
  10. package/dist/{core-SKRPJQZG.js → core-W2HYIQW6.js} +2 -2
  11. package/dist/{generate-7AF7WRVK.js → generate-LMTISDIJ.js} +3 -3
  12. package/dist/index.js +3 -3
  13. package/dist/{init-WKGDPYI4.js → init-7CHRKQ7P.js} +3 -3
  14. package/dist/mcp-bin.js +2 -2
  15. package/dist/{scan-K6JNMCGM.js → scan-WY23TJCP.js} +4 -4
  16. package/dist/{service-F3E4JJM7.js → service-T2L7VLTE.js} +2 -2
  17. package/dist/{static-viewer-4LQZ5AGA.js → static-viewer-GBR7YNF3.js} +2 -2
  18. package/dist/{test-CJDNJTPZ.js → test-OJRXNDO2.js} +2 -2
  19. package/dist/{tokens-JAJABYXP.js → tokens-3BWDESVM.js} +3 -3
  20. package/dist/{viewer-R3Q6WAMJ.js → viewer-SUFOISZM.js} +12 -12
  21. package/package.json +2 -2
  22. package/src/bin.ts +23 -0
  23. package/src/build.ts +43 -0
  24. package/src/commands/graph.ts +274 -0
  25. package/src/core/composition.ts +64 -1
  26. package/src/core/graph-extractor.test.ts +542 -0
  27. package/src/core/graph-extractor.ts +601 -0
  28. package/src/core/importAnalyzer.ts +5 -0
  29. package/src/viewer/components/App.tsx +128 -30
  30. package/src/viewer/components/Icons.tsx +53 -1
  31. package/src/viewer/components/Layout.tsx +7 -3
  32. package/src/viewer/components/LeftSidebar.tsx +65 -87
  33. package/src/viewer/components/PreviewFrameHost.tsx +30 -1
  34. package/src/viewer/components/PreviewToolbar.tsx +57 -10
  35. package/src/viewer/components/ViewportSelector.tsx +56 -45
  36. package/src/viewer/constants/ui.ts +4 -4
  37. package/src/viewer/preview-frame.html +22 -13
  38. package/src/viewer/styles/globals.css +42 -81
  39. package/dist/chunk-D35RGPAG.js.map +0 -1
  40. /package/dist/{chunk-F7ITZPDJ.js.map → chunk-XHUDJNN3.js.map} +0 -0
  41. /package/dist/{core-SKRPJQZG.js.map → core-W2HYIQW6.js.map} +0 -0
  42. /package/dist/{generate-7AF7WRVK.js.map → generate-LMTISDIJ.js.map} +0 -0
  43. /package/dist/{init-WKGDPYI4.js.map → init-7CHRKQ7P.js.map} +0 -0
  44. /package/dist/{scan-K6JNMCGM.js.map → scan-WY23TJCP.js.map} +0 -0
  45. /package/dist/{service-F3E4JJM7.js.map → service-T2L7VLTE.js.map} +0 -0
  46. /package/dist/{static-viewer-4LQZ5AGA.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  47. /package/dist/{test-CJDNJTPZ.js.map → test-OJRXNDO2.js.map} +0 -0
  48. /package/dist/{tokens-JAJABYXP.js.map → tokens-3BWDESVM.js.map} +0 -0
  49. /package/dist/{viewer-R3Q6WAMJ.js.map → viewer-SUFOISZM.js.map} +0 -0
@@ -6,10 +6,10 @@ import {
6
6
  findStorybookDir,
7
7
  generatePreviewModule,
8
8
  loadConfig
9
- } from "./chunk-SSLQXHNX.js";
9
+ } from "./chunk-CVXKXVOY.js";
10
10
  import {
11
11
  generateContext
12
- } from "./chunk-Q7GOHVOK.js";
12
+ } from "./chunk-TJ34N7C7.js";
13
13
  import {
14
14
  BRAND
15
15
  } from "./chunk-6JBGU74P.js";
@@ -240,7 +240,7 @@ var sharedRenderPool = null;
240
240
  var browserPoolModule = null;
241
241
  async function getSharedRenderPool() {
242
242
  if (!browserPoolModule) {
243
- browserPoolModule = await import("./service-F3E4JJM7.js");
243
+ browserPoolModule = await import("./service-T2L7VLTE.js");
244
244
  }
245
245
  if (!sharedRenderPool) {
246
246
  sharedRenderPool = new browserPoolModule.BrowserPool({
@@ -469,7 +469,7 @@ function segmentsPlugin(options) {
469
469
  const address = _server.httpServer?.address();
470
470
  const port = typeof address === "object" && address ? address.port : 6006;
471
471
  const renderViewport = viewport || { width: 800, height: 600 };
472
- const { FigmaClient, bufferToBase64Url } = await import("./service-F3E4JJM7.js");
472
+ const { FigmaClient, bufferToBase64Url } = await import("./service-T2L7VLTE.js");
473
473
  const figmaClient = new FigmaClient({
474
474
  accessToken: figmaToken
475
475
  });
@@ -560,7 +560,7 @@ function segmentsPlugin(options) {
560
560
  );
561
561
  return;
562
562
  }
563
- const { FigmaClient } = await import("./service-F3E4JJM7.js");
563
+ const { FigmaClient } = await import("./service-T2L7VLTE.js");
564
564
  const figmaClient = new FigmaClient({ accessToken: figmaToken });
565
565
  const { fileKey, nodeId } = figmaClient.parseUrl(figmaUrl);
566
566
  const figmaDesignProps = await figmaClient.getNodeProperties(
@@ -601,7 +601,7 @@ function segmentsPlugin(options) {
601
601
  }));
602
602
  return;
603
603
  }
604
- const { getSharedTokenRegistry } = await import("./service-F3E4JJM7.js");
604
+ const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
605
605
  const registry = getSharedTokenRegistry();
606
606
  if (!registry.isInitialized()) {
607
607
  await registry.initialize(config.tokens, projectRoot);
@@ -661,7 +661,7 @@ function segmentsPlugin(options) {
661
661
  }));
662
662
  return;
663
663
  }
664
- const { getSharedTokenRegistry } = await import("./service-F3E4JJM7.js");
664
+ const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
665
665
  const registry = getSharedTokenRegistry();
666
666
  if (!registry.isInitialized()) {
667
667
  await registry.initialize(config.tokens, projectRoot);
@@ -723,7 +723,7 @@ function segmentsPlugin(options) {
723
723
  res.end(JSON.stringify({ error: "Could not resolve segment file path" }));
724
724
  return;
725
725
  }
726
- const { getSharedTokenRegistry } = await import("./service-F3E4JJM7.js");
726
+ const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
727
727
  const registry = getSharedTokenRegistry();
728
728
  if (!registry.isInitialized()) {
729
729
  await registry.initialize(config.tokens, projectRoot);
@@ -849,7 +849,7 @@ function segmentsPlugin(options) {
849
849
  }
850
850
  const { writeFile, mkdir } = await import("fs/promises");
851
851
  const { join: join2 } = await import("path");
852
- const { BRAND: BRAND2 } = await import("./core-SKRPJQZG.js");
852
+ const { BRAND: BRAND2 } = await import("./core-W2HYIQW6.js");
853
853
  const fragmentsDir = join2(projectRoot, BRAND2.dataDir, BRAND2.componentsDir);
854
854
  await mkdir(fragmentsDir, { recursive: true });
855
855
  const fragmentPath = join2(
@@ -904,7 +904,7 @@ function segmentsPlugin(options) {
904
904
  const {
905
905
  getSharedTokenRegistry,
906
906
  generateTokenPatches
907
- } = await import("./service-F3E4JJM7.js");
907
+ } = await import("./service-T2L7VLTE.js");
908
908
  const registry = getSharedTokenRegistry();
909
909
  if (!registry.isInitialized()) {
910
910
  await registry.initialize(config.tokens, projectRoot);
@@ -1566,7 +1566,7 @@ async function loadFullSegmentForCompare(_server, _segmentFiles, componentName,
1566
1566
  }
1567
1567
  }
1568
1568
  async function compareImages(image1Base64, image2Base64, threshold) {
1569
- const { DiffEngine, base64UrlToBuffer, bufferToBase64Url } = await import("./service-F3E4JJM7.js");
1569
+ const { DiffEngine, base64UrlToBuffer, bufferToBase64Url } = await import("./service-T2L7VLTE.js");
1570
1570
  const { PNG } = await import("pngjs");
1571
1571
  const buffer1 = base64UrlToBuffer(image1Base64);
1572
1572
  const buffer2 = base64UrlToBuffer(image2Base64);
@@ -1819,4 +1819,4 @@ export {
1819
1819
  createDevServer,
1820
1820
  segmentsPlugin
1821
1821
  };
1822
- //# sourceMappingURL=viewer-R3Q6WAMJ.js.map
1822
+ //# sourceMappingURL=viewer-SUFOISZM.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI, MCP server, and dev tools for Fragments design system",
5
5
  "repository": {
6
6
  "type": "git",
@@ -65,7 +65,7 @@
65
65
  "vite": "^6.0.0",
66
66
  "vite-plugin-svgr": "^4.5.0",
67
67
  "zod": "^3.24.1",
68
- "@fragments-sdk/context": "0.2.0"
68
+ "@fragments-sdk/context": "0.3.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/babel__generator": "^7.6.8",
package/src/bin.ts CHANGED
@@ -35,6 +35,7 @@ import { add } from './commands/add.js';
35
35
  import { linkFigma, linkStorybook } from './commands/link/index.js';
36
36
  import { enhance } from './commands/enhance.js';
37
37
  import { scan } from './commands/scan.js';
38
+ import { graph } from './commands/graph.js';
38
39
 
39
40
  // Import existing commands that were already extracted
40
41
  import { runScreenshotCommand } from './screenshot.js';
@@ -844,6 +845,28 @@ program
844
845
  }
845
846
  });
846
847
 
848
+ // ============================================================================
849
+ // GRAPH COMMAND
850
+ // ============================================================================
851
+ program
852
+ .command('graph')
853
+ .description('Query the component relationship graph')
854
+ .argument('[component]', 'Component name (optional)')
855
+ .option('-c, --config <path>', 'Path to config file')
856
+ .option('-m, --mode <mode>', 'Query mode: health, dependencies, dependents, impact, path, composition, alternatives, islands')
857
+ .option('-t, --target <component>', 'Target component for path mode')
858
+ .option('--edge-types <types>', 'Comma-separated edge types to filter by')
859
+ .option('--depth <number>', 'Max traversal depth for impact mode', parseInt)
860
+ .option('--format <format>', 'Output format: table, json, dot', 'table')
861
+ .action(async (component, options) => {
862
+ try {
863
+ await graph(component, options);
864
+ } catch (error) {
865
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
866
+ process.exit(1);
867
+ }
868
+ });
869
+
847
870
  // ============================================================================
848
871
  // TEST COMMAND
849
872
  // ============================================================================
package/src/build.ts CHANGED
@@ -24,6 +24,8 @@ import {
24
24
  resolveComponentSourcePath,
25
25
  type AutoDetectedPropDefinition,
26
26
  } from "./core/auto-props.js";
27
+ import { buildComponentGraph } from "./core/graph-extractor.js";
28
+ import { serializeGraph } from "@fragments-sdk/context/graph";
27
29
 
28
30
  type CompiledProp = CompiledSegment["props"][string];
29
31
 
@@ -295,6 +297,46 @@ export async function buildSegments(
295
297
  }
296
298
  }
297
299
 
300
+ // Build component graph for AI structural queries
301
+ // Derive component directory from configDir (typically src/components/)
302
+ const componentDir = resolve(configDir, "src", "components");
303
+ let graphData: ReturnType<typeof serializeGraph> | undefined;
304
+ try {
305
+ const graphResult = await buildComponentGraph(segments, blocks, componentDir);
306
+
307
+ // Auto-enrich segments with detected metadata
308
+ for (const [name, segment] of Object.entries(segments)) {
309
+ const detected = graphResult.autoDetected.get(name);
310
+ if (!detected) continue;
311
+
312
+ if (!segment.ai) segment.ai = {};
313
+ if (!segment.ai.subComponents && detected.subComponents) {
314
+ segment.ai.subComponents = detected.subComponents;
315
+ }
316
+ if (!segment.ai.compositionPattern && detected.compositionPattern) {
317
+ segment.ai.compositionPattern = detected.compositionPattern;
318
+ }
319
+ if (!segment.ai.commonPatterns && detected.commonPatterns) {
320
+ segment.ai.commonPatterns = detected.commonPatterns;
321
+ }
322
+ if (!segment.ai.requiredChildren && detected.requiredChildren) {
323
+ segment.ai.requiredChildren = detected.requiredChildren;
324
+ }
325
+ }
326
+
327
+ // Report drift warnings
328
+ for (const w of graphResult.warnings) {
329
+ warnings.push({ file: "graph", warning: w });
330
+ }
331
+
332
+ graphData = serializeGraph(graphResult.graph);
333
+ } catch (error) {
334
+ warnings.push({
335
+ file: "graph",
336
+ warning: `Graph extraction failed: ${error instanceof Error ? error.message : String(error)}`,
337
+ });
338
+ }
339
+
298
340
  const output: CompiledSegmentsFile = {
299
341
  version: "1.0.0",
300
342
  generatedAt: new Date().toISOString(),
@@ -302,6 +344,7 @@ export async function buildSegments(
302
344
  segments,
303
345
  ...(Object.keys(blocks).length > 0 && { blocks }),
304
346
  ...(tokens && { tokens }),
347
+ ...(graphData && { graph: graphData }),
305
348
  };
306
349
 
307
350
  const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `fragments graph` — query the component relationship graph from the CLI.
3
+ *
4
+ * Loads fragments.json, instantiates the ComponentGraphEngine, and runs
5
+ * the requested query mode. Output formats: table (colored terminal), json, dot.
6
+ */
7
+
8
+ import pc from 'picocolors';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
11
+ import type { CompiledSegmentsFile } from '../core/index.js';
12
+ import { BRAND } from '../core/index.js';
13
+ import { loadConfig } from '../core/node.js';
14
+ import {
15
+ ComponentGraphEngine,
16
+ deserializeGraph,
17
+ } from '@fragments-sdk/context/graph';
18
+ import type { GraphEdgeType } from '@fragments-sdk/context/graph';
19
+
20
+ export interface GraphCommandOptions {
21
+ config?: string;
22
+ mode?: string;
23
+ target?: string;
24
+ edgeTypes?: string;
25
+ depth?: number;
26
+ format?: 'table' | 'json' | 'dot';
27
+ }
28
+
29
+ export async function graph(
30
+ component: string | undefined,
31
+ options: GraphCommandOptions,
32
+ ): Promise<void> {
33
+ const { config, configDir } = await loadConfig(options.config);
34
+ const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
35
+
36
+ let data: CompiledSegmentsFile;
37
+ try {
38
+ const content = await readFile(outputPath, 'utf-8');
39
+ data = JSON.parse(content) as CompiledSegmentsFile;
40
+ } catch {
41
+ console.error(
42
+ pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ if (!data.graph) {
48
+ console.error(
49
+ pc.red(`Error: No graph data in ${BRAND.outFile}. Rebuild with the latest CLI.`),
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const graph = deserializeGraph(data.graph);
55
+ const blocks = data.blocks
56
+ ? Object.fromEntries(
57
+ Object.entries(data.blocks).map(([k, v]) => [k, { components: v.components }]),
58
+ )
59
+ : undefined;
60
+ const engine = new ComponentGraphEngine(graph, blocks);
61
+
62
+ const mode = options.mode ?? (component ? 'dependencies' : 'health');
63
+ const format = options.format ?? 'table';
64
+ const edgeTypes = options.edgeTypes
65
+ ? (options.edgeTypes.split(',') as GraphEdgeType[])
66
+ : undefined;
67
+
68
+ // Validate component exists for modes that require it
69
+ const needsComponent = ['dependencies', 'dependents', 'impact', 'path', 'composition', 'alternatives'];
70
+ if (needsComponent.includes(mode) && !component) {
71
+ console.error(pc.red(`Error: "${mode}" mode requires a component name.`));
72
+ process.exit(1);
73
+ }
74
+ if (component && !engine.hasNode(component)) {
75
+ console.error(pc.red(`Error: Component "${component}" not found in graph.`));
76
+ process.exit(1);
77
+ }
78
+
79
+ switch (mode) {
80
+ case 'health': {
81
+ const health = engine.getHealth();
82
+ if (format === 'json') {
83
+ console.log(JSON.stringify(health, null, 2));
84
+ return;
85
+ }
86
+
87
+ console.log(pc.bold('\nComponent Graph Health\n'));
88
+ console.log(` ${pc.cyan('Nodes:')} ${health.nodeCount}`);
89
+ console.log(` ${pc.cyan('Edges:')} ${health.edgeCount}`);
90
+ console.log(` ${pc.cyan('Avg degree:')} ${health.averageDegree}`);
91
+ console.log(` ${pc.cyan('Islands:')} ${health.connectedComponents.length}`);
92
+ console.log(` ${pc.cyan('Coverage:')} ${health.compositionCoverage}% in blocks`);
93
+ console.log(` ${pc.cyan('Orphans:')} ${health.orphans.length > 0 ? health.orphans.join(', ') : pc.green('none')}`);
94
+
95
+ if (health.hubs.length > 0) {
96
+ console.log(`\n ${pc.bold('Top hubs:')}`);
97
+ for (const hub of health.hubs.slice(0, 5)) {
98
+ console.log(` ${pc.yellow(hub.name)} — ${hub.degree} connections`);
99
+ }
100
+ }
101
+ console.log();
102
+ break;
103
+ }
104
+
105
+ case 'dependencies': {
106
+ const deps = engine.dependencies(component!, edgeTypes);
107
+ if (format === 'json') {
108
+ console.log(JSON.stringify({ component, dependencies: deps }, null, 2));
109
+ return;
110
+ }
111
+
112
+ console.log(pc.bold(`\nDependencies of ${component}\n`));
113
+ if (deps.length === 0) {
114
+ console.log(' No outgoing dependencies.');
115
+ } else {
116
+ for (const dep of deps) {
117
+ console.log(` ${pc.yellow(dep.target)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
118
+ }
119
+ }
120
+ console.log();
121
+ break;
122
+ }
123
+
124
+ case 'dependents': {
125
+ const deps = engine.dependents(component!, edgeTypes);
126
+ if (format === 'json') {
127
+ console.log(JSON.stringify({ component, dependents: deps }, null, 2));
128
+ return;
129
+ }
130
+
131
+ console.log(pc.bold(`\nDependents of ${component}\n`));
132
+ if (deps.length === 0) {
133
+ console.log(' No incoming dependents.');
134
+ } else {
135
+ for (const dep of deps) {
136
+ console.log(` ${pc.yellow(dep.source)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
137
+ }
138
+ }
139
+ console.log();
140
+ break;
141
+ }
142
+
143
+ case 'impact': {
144
+ const result = engine.impact(component!, options.depth ?? 3);
145
+ if (format === 'json') {
146
+ console.log(JSON.stringify(result, null, 2));
147
+ return;
148
+ }
149
+
150
+ console.log(pc.bold(`\nImpact analysis: ${component}\n`));
151
+ console.log(` ${pc.red(`${result.totalAffected}`)} affected components, ${pc.red(`${result.affectedBlocks.length}`)} affected blocks\n`);
152
+
153
+ if (result.affected.length > 0) {
154
+ console.log(` ${pc.bold('Affected components:')}`);
155
+ for (const entry of result.affected) {
156
+ const indent = ' '.repeat(entry.depth + 1);
157
+ console.log(`${indent}${pc.yellow(entry.component)} ${pc.dim(`(depth ${entry.depth}, via ${entry.edgeType})`)}`);
158
+ }
159
+ }
160
+
161
+ if (result.affectedBlocks.length > 0) {
162
+ console.log(`\n ${pc.bold('Affected blocks:')} ${result.affectedBlocks.join(', ')}`);
163
+ }
164
+ console.log();
165
+ break;
166
+ }
167
+
168
+ case 'path': {
169
+ if (!options.target) {
170
+ console.error(pc.red('Error: --target is required for path mode.'));
171
+ process.exit(1);
172
+ }
173
+
174
+ const result = engine.path(component!, options.target);
175
+ if (format === 'json') {
176
+ console.log(JSON.stringify(result, null, 2));
177
+ return;
178
+ }
179
+
180
+ console.log(pc.bold(`\nPath: ${component} → ${options.target}\n`));
181
+ if (!result.found) {
182
+ console.log(` ${pc.red('No path found.')}`);
183
+ } else {
184
+ console.log(` ${result.path.map(n => pc.yellow(n)).join(pc.dim(' → '))}`);
185
+ if (result.edges.length > 0) {
186
+ console.log(` ${pc.dim(`(${result.edges.map(e => e.type).join(' → ')})`)}`);
187
+ }
188
+ }
189
+ console.log();
190
+ break;
191
+ }
192
+
193
+ case 'composition': {
194
+ const tree = engine.composition(component!);
195
+ if (format === 'json') {
196
+ console.log(JSON.stringify(tree, null, 2));
197
+ return;
198
+ }
199
+
200
+ console.log(pc.bold(`\nComposition: ${component}\n`));
201
+ console.log(` ${pc.cyan('Pattern:')} ${tree.compositionPattern ?? 'unknown'}`);
202
+ if (tree.parent) {
203
+ console.log(` ${pc.cyan('Parent:')} ${tree.parent}`);
204
+ }
205
+ if (tree.subComponents.length > 0) {
206
+ console.log(` ${pc.cyan('Sub-components:')}`);
207
+ for (const sub of tree.subComponents) {
208
+ const isRequired = tree.requiredChildren.includes(sub);
209
+ console.log(` ${pc.yellow(sub)}${isRequired ? pc.red(' (required)') : ''}`);
210
+ }
211
+ }
212
+ if (tree.siblings.length > 0) {
213
+ console.log(` ${pc.cyan('Siblings:')} ${tree.siblings.join(', ')}`);
214
+ }
215
+ if (tree.blocks.length > 0) {
216
+ console.log(` ${pc.cyan('In blocks:')} ${tree.blocks.join(', ')}`);
217
+ }
218
+ console.log();
219
+ break;
220
+ }
221
+
222
+ case 'alternatives': {
223
+ const alts = engine.alternatives(component!);
224
+ if (format === 'json') {
225
+ console.log(JSON.stringify({ component, alternatives: alts }, null, 2));
226
+ return;
227
+ }
228
+
229
+ console.log(pc.bold(`\nAlternatives for ${component}\n`));
230
+ if (alts.length === 0) {
231
+ console.log(' No known alternatives.');
232
+ } else {
233
+ for (const alt of alts) {
234
+ console.log(` ${pc.yellow(alt.component)}${alt.note ? ` — ${alt.note}` : ''}`);
235
+ }
236
+ }
237
+ console.log();
238
+ break;
239
+ }
240
+
241
+ case 'islands': {
242
+ const islands = engine.islands();
243
+ if (format === 'json') {
244
+ console.log(JSON.stringify({ islands }, null, 2));
245
+ return;
246
+ }
247
+
248
+ console.log(pc.bold(`\nConnected Islands (${islands.length})\n`));
249
+ for (let i = 0; i < islands.length; i++) {
250
+ console.log(` ${pc.cyan(`Island ${i + 1}`)} (${islands[i].length} components): ${islands[i].join(', ')}`);
251
+ }
252
+ console.log();
253
+ break;
254
+ }
255
+
256
+ default:
257
+ console.error(pc.red(`Unknown mode: "${mode}". Valid: health, dependencies, dependents, impact, path, composition, alternatives, islands`));
258
+ process.exit(1);
259
+ }
260
+
261
+ // Graphviz dot output
262
+ if (format === 'dot') {
263
+ const lines = ['digraph ComponentGraph {', ' rankdir=LR;', ' node [shape=box, style=rounded];'];
264
+ for (const node of graph.nodes) {
265
+ lines.push(` "${node.name}" [label="${node.name}\\n(${node.category})"];`);
266
+ }
267
+ for (const edge of graph.edges) {
268
+ const style = edge.type === 'alternative-to' ? 'dashed' : 'solid';
269
+ lines.push(` "${edge.source}" -> "${edge.target}" [label="${edge.type}", style=${style}];`);
270
+ }
271
+ lines.push('}');
272
+ console.log(lines.join('\n'));
273
+ }
274
+ }
@@ -1,4 +1,6 @@
1
1
  import type { CompiledSegment, RelationshipType } from "./types.js";
2
+ import type { ComponentGraph } from "@fragments-sdk/context/graph";
3
+ import { ComponentGraphEngine } from "@fragments-sdk/context/graph";
2
4
 
3
5
  // --- Public types ---
4
6
 
@@ -58,12 +60,16 @@ const CATEGORY_AFFINITIES: Record<string, string[]> = {
58
60
  * Returns warnings about missing relations, usage conflicts,
59
61
  * and suggestions for additional components.
60
62
  *
63
+ * When a ComponentGraph is provided via `options.graph`, the analysis is
64
+ * enhanced with graph-based dependency detection and block-based suggestions.
65
+ *
61
66
  * Browser-safe: no Node.js APIs used.
62
67
  */
63
68
  export function analyzeComposition(
64
69
  segments: Record<string, CompiledSegment>,
65
70
  componentNames: string[],
66
- _context?: string
71
+ _context?: string,
72
+ options?: { graph?: ComponentGraph },
67
73
  ): CompositionAnalysis {
68
74
  const allNames = new Set(Object.keys(segments));
69
75
 
@@ -218,6 +224,63 @@ export function analyzeComposition(
218
224
  }
219
225
  }
220
226
 
227
+ // 6. Graph-enhanced analysis (when graph data is available)
228
+ if (options?.graph) {
229
+ const engine = new ComponentGraphEngine(options.graph);
230
+
231
+ // Add graph-based dependency warnings
232
+ for (const name of components) {
233
+ const deps = engine.dependencies(name, ["imports", "hook-depends"]);
234
+ for (const dep of deps) {
235
+ if (
236
+ !selectedSet.has(dep.target) &&
237
+ !suggestedSet.has(dep.target) &&
238
+ allNames.has(dep.target)
239
+ ) {
240
+ suggestions.push({
241
+ component: dep.target,
242
+ reason: `"${name}" ${dep.type === "hook-depends" ? "uses a hook from" : "imports"} "${dep.target}"`,
243
+ relationship: "composition",
244
+ sourceComponent: name,
245
+ });
246
+ suggestedSet.add(dep.target);
247
+ }
248
+ }
249
+ }
250
+
251
+ // Add block-based suggestions
252
+ for (const name of components) {
253
+ const blocks = engine.blocksUsing(name);
254
+ for (const blockName of blocks) {
255
+ // Find other components in this block that aren't selected
256
+ const blockComps = options.graph.edges
257
+ .filter(
258
+ (e) =>
259
+ e.type === "composes" &&
260
+ e.provenance === `block:${blockName}` &&
261
+ (e.source === name || e.target === name)
262
+ )
263
+ .map((e) => (e.source === name ? e.target : e.source));
264
+
265
+ for (const comp of blockComps) {
266
+ if (
267
+ !selectedSet.has(comp) &&
268
+ !suggestedSet.has(comp) &&
269
+ allNames.has(comp)
270
+ ) {
271
+ suggestions.push({
272
+ component: comp,
273
+ reason: `"${name}" and "${comp}" are used together in the "${blockName}" block`,
274
+ relationship: "composition",
275
+ sourceComponent: name,
276
+ });
277
+ suggestedSet.add(comp);
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+
221
284
  return { components, unknown, warnings, suggestions, guidelines };
222
285
  }
223
286