@fragments-sdk/cli 0.15.0 → 0.15.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.
Files changed (118) hide show
  1. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  2. package/dist/bin.js +463 -71
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-5JF26E55.js +1255 -0
  5. package/dist/chunk-5JF26E55.js.map +1 -0
  6. package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
  7. package/dist/chunk-6SQPP47U.js.map +1 -0
  8. package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
  9. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  10. package/dist/chunk-MHIBEEW4.js +511 -0
  11. package/dist/chunk-MHIBEEW4.js.map +1 -0
  12. package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
  13. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  14. package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
  15. package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
  16. package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
  17. package/dist/core/index.js +0 -1
  18. package/dist/create-IH4R45GE.js +806 -0
  19. package/dist/create-IH4R45GE.js.map +1 -0
  20. package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
  21. package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
  22. package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
  23. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  24. package/dist/index.d.ts +2 -22
  25. package/dist/index.js +8 -7
  26. package/dist/index.js.map +1 -1
  27. package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
  28. package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
  29. package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
  30. package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  31. package/dist/mcp-bin.js +1 -2
  32. package/dist/mcp-bin.js.map +1 -1
  33. package/dist/node-37AUE74M.js +65 -0
  34. package/dist/push-contracts-WY32TFP6.js +84 -0
  35. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  36. package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
  37. package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
  38. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  39. package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
  40. package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
  41. package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
  42. package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  43. package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
  44. package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
  45. package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
  46. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  47. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  48. package/dist/token-parser-32KOIOFN.js +22 -0
  49. package/dist/token-parser-32KOIOFN.js.map +1 -0
  50. package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
  51. package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
  52. package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
  53. package/dist/tokens-push-HY3KO36V.js +148 -0
  54. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  55. package/package.json +5 -3
  56. package/src/bin.ts +90 -0
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  59. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  60. package/src/commands/__tests__/create.test.ts +71 -0
  61. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  62. package/src/commands/__tests__/govern.test.ts +258 -0
  63. package/src/commands/__tests__/init.test.ts +1 -1
  64. package/src/commands/__tests__/scan-generate.test.ts +1 -1
  65. package/src/commands/build.ts +54 -1
  66. package/src/commands/context.ts +1 -1
  67. package/src/commands/create.ts +536 -0
  68. package/src/commands/doctor.ts +3 -2
  69. package/src/commands/govern-scan.ts +187 -8
  70. package/src/commands/govern.ts +65 -2
  71. package/src/commands/init-cloud.ts +32 -4
  72. package/src/commands/push-contracts.ts +112 -0
  73. package/src/commands/scan-generate.ts +1 -1
  74. package/src/commands/scan.ts +13 -0
  75. package/src/commands/sync.ts +2 -2
  76. package/src/commands/tokens-push.ts +199 -0
  77. package/src/core/__tests__/token-resolver.test.ts +1 -1
  78. package/src/core/component-extractor.test.ts +1 -1
  79. package/src/core/drift-verifier.ts +1 -1
  80. package/src/core/extractor-adapter.ts +1 -1
  81. package/src/index.ts +3 -3
  82. package/src/migrate/fragment-to-contract.ts +2 -2
  83. package/src/service/index.ts +8 -0
  84. package/src/service/tailwind-v4-parser.ts +314 -0
  85. package/src/service/token-parser.ts +56 -0
  86. package/src/setup.ts +10 -39
  87. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  88. package/src/theme/__tests__/serializer.test.ts +1 -1
  89. package/src/theme/generator.ts +16 -1
  90. package/src/theme/schema.ts +8 -0
  91. package/src/theme/serializer.ts +13 -9
  92. package/src/theme/types.ts +8 -0
  93. package/src/validators.ts +1 -2
  94. package/dist/chunk-65WSVDV5.js.map +0 -1
  95. package/dist/chunk-7WHVW72L.js +0 -2664
  96. package/dist/chunk-7WHVW72L.js.map +0 -1
  97. package/dist/chunk-CZD3AD4Q.js.map +0 -1
  98. package/dist/chunk-MN3TJ3D5.js +0 -695
  99. package/dist/chunk-MN3TJ3D5.js.map +0 -1
  100. package/dist/chunk-XJQ5BIWI.js.map +0 -1
  101. package/dist/chunk-Z7EY4VHE.js +0 -50
  102. package/dist/govern-scan-UCBZR6D6.js.map +0 -1
  103. package/dist/sass.node-4XJK6YBF.js +0 -130708
  104. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  105. package/dist/scan-generate-TWRHNU5M.js.map +0 -1
  106. package/src/build.ts +0 -736
  107. package/src/core/auto-props.ts +0 -464
  108. package/src/core/component-extractor.ts +0 -1121
  109. package/src/core/token-resolver.ts +0 -155
  110. package/src/viewer/preview-adapter.ts +0 -116
  111. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  112. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  113. /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
  114. /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
  115. /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
  116. /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
  117. /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
  118. /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
@@ -0,0 +1,258 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ describe('govern init', () => {
8
+ let tmpDir: string;
9
+ let originalCwd: string;
10
+
11
+ beforeAll(async () => {
12
+ tmpDir = await mkdtemp(join(tmpdir(), 'fragments-govern-test-'));
13
+ originalCwd = process.cwd();
14
+ });
15
+
16
+ afterAll(async () => {
17
+ process.chdir(originalCwd);
18
+ await rm(tmpDir, { recursive: true, force: true });
19
+ });
20
+
21
+ it(
22
+ 'generates a fragments.config.ts with default output path',
23
+ { timeout: 15_000 },
24
+ async () => {
25
+ const projectDir = join(tmpDir, 'default-init');
26
+ await mkdir(projectDir, { recursive: true });
27
+ process.chdir(projectDir);
28
+
29
+ const { governInit } = await import('../govern.js');
30
+ await governInit();
31
+
32
+ const configPath = join(projectDir, 'fragments.config.ts');
33
+ expect(existsSync(configPath)).toBe(true);
34
+
35
+ const content = await readFile(configPath, 'utf-8');
36
+ expect(content).toContain('rules');
37
+ },
38
+ );
39
+
40
+ it(
41
+ 'generates config at custom output path',
42
+ { timeout: 15_000 },
43
+ async () => {
44
+ const projectDir = join(tmpDir, 'custom-output');
45
+ await mkdir(projectDir, { recursive: true });
46
+ process.chdir(projectDir);
47
+
48
+ const customPath = join(projectDir, 'my-config.ts');
49
+ const { governInit } = await import('../govern.js');
50
+ await governInit({ output: customPath });
51
+
52
+ expect(existsSync(customPath)).toBe(true);
53
+ const content = await readFile(customPath, 'utf-8');
54
+ expect(content).toContain('rules');
55
+ },
56
+ );
57
+
58
+ it(
59
+ 'auto-detects Tailwind config and includes it in token sources',
60
+ { timeout: 15_000 },
61
+ async () => {
62
+ const projectDir = join(tmpDir, 'tailwind-detect');
63
+ await mkdir(projectDir, { recursive: true });
64
+ process.chdir(projectDir);
65
+
66
+ // Create a tailwind config file
67
+ await writeFile(
68
+ join(projectDir, 'tailwind.config.ts'),
69
+ 'export default { content: ["./src/**/*.tsx"] };',
70
+ );
71
+
72
+ const { governInit } = await import('../govern.js');
73
+ await governInit();
74
+
75
+ const configPath = join(projectDir, 'fragments.config.ts');
76
+ const content = await readFile(configPath, 'utf-8');
77
+ expect(content).toContain('tailwind.config.ts');
78
+ },
79
+ );
80
+
81
+ it(
82
+ 'auto-detects SCSS token files',
83
+ { timeout: 15_000 },
84
+ async () => {
85
+ const projectDir = join(tmpDir, 'scss-detect');
86
+ await mkdir(join(projectDir, 'src', 'styles'), { recursive: true });
87
+ process.chdir(projectDir);
88
+
89
+ await writeFile(
90
+ join(projectDir, 'src', 'styles', 'tokens.scss'),
91
+ '$color-primary: #2563EB;',
92
+ );
93
+
94
+ const { governInit } = await import('../govern.js');
95
+ await governInit();
96
+
97
+ const configPath = join(projectDir, 'fragments.config.ts');
98
+ const content = await readFile(configPath, 'utf-8');
99
+ expect(content).toContain('src/styles/tokens.scss');
100
+ },
101
+ );
102
+
103
+ it(
104
+ 'handles empty project gracefully (no token sources)',
105
+ { timeout: 15_000 },
106
+ async () => {
107
+ const projectDir = join(tmpDir, 'empty-project');
108
+ await mkdir(projectDir, { recursive: true });
109
+ process.chdir(projectDir);
110
+
111
+ const { governInit } = await import('../govern.js');
112
+ await governInit();
113
+
114
+ const configPath = join(projectDir, 'fragments.config.ts');
115
+ expect(existsSync(configPath)).toBe(true);
116
+ },
117
+ );
118
+ });
119
+
120
+ describe('govern check', () => {
121
+ let tmpDir: string;
122
+ let originalCwd: string;
123
+
124
+ beforeAll(async () => {
125
+ tmpDir = await mkdtemp(join(tmpdir(), 'fragments-govern-check-'));
126
+ originalCwd = process.cwd();
127
+ });
128
+
129
+ afterAll(async () => {
130
+ process.chdir(originalCwd);
131
+ await rm(tmpDir, { recursive: true, force: true });
132
+ });
133
+
134
+ it(
135
+ 'returns exitCode 0 for a passing spec',
136
+ { timeout: 15_000 },
137
+ async () => {
138
+ const projectDir = join(tmpDir, 'check-pass');
139
+ await mkdir(projectDir, { recursive: true });
140
+ process.chdir(projectDir);
141
+
142
+ // Write a minimal UISpec that should pass basic checks
143
+ const spec = {
144
+ nodes: [
145
+ {
146
+ id: 'root',
147
+ type: 'div',
148
+ props: {},
149
+ children: ['btn'],
150
+ },
151
+ {
152
+ id: 'btn',
153
+ type: 'Button',
154
+ props: { variant: 'primary' },
155
+ },
156
+ ],
157
+ root: 'root',
158
+ };
159
+ await writeFile(
160
+ join(projectDir, 'spec.json'),
161
+ JSON.stringify(spec, null, 2),
162
+ );
163
+
164
+ const { governCheck } = await import('../govern.js');
165
+ const result = await governCheck({
166
+ input: join(projectDir, 'spec.json'),
167
+ quiet: true,
168
+ });
169
+
170
+ // With no config (empty rules), should pass
171
+ expect(typeof result.exitCode).toBe('number');
172
+ expect(result.exitCode === 0 || result.exitCode === 1).toBe(true);
173
+ },
174
+ );
175
+
176
+ it(
177
+ 'returns a valid result structure',
178
+ { timeout: 15_000 },
179
+ async () => {
180
+ const projectDir = join(tmpDir, 'check-structure');
181
+ await mkdir(projectDir, { recursive: true });
182
+ process.chdir(projectDir);
183
+
184
+ const spec = {
185
+ nodes: [{ id: 'root', type: 'div' }],
186
+ root: 'root',
187
+ };
188
+ await writeFile(
189
+ join(projectDir, 'spec.json'),
190
+ JSON.stringify(spec, null, 2),
191
+ );
192
+
193
+ const { governCheck } = await import('../govern.js');
194
+ const result = await governCheck({
195
+ input: join(projectDir, 'spec.json'),
196
+ quiet: true,
197
+ });
198
+
199
+ expect(result).toHaveProperty('exitCode');
200
+ },
201
+ );
202
+ });
203
+
204
+ describe('govern report', () => {
205
+ let tmpDir: string;
206
+ let originalCwd: string;
207
+
208
+ beforeAll(async () => {
209
+ tmpDir = await mkdtemp(join(tmpdir(), 'fragments-govern-report-'));
210
+ originalCwd = process.cwd();
211
+ });
212
+
213
+ afterAll(async () => {
214
+ process.chdir(originalCwd);
215
+ await rm(tmpDir, { recursive: true, force: true });
216
+ });
217
+
218
+ it(
219
+ 'handles missing audit log gracefully',
220
+ { timeout: 10_000 },
221
+ async () => {
222
+ const projectDir = join(tmpDir, 'no-log');
223
+ await mkdir(projectDir, { recursive: true });
224
+ process.chdir(projectDir);
225
+
226
+ const { governReport } = await import('../govern.js');
227
+ // Should not throw
228
+ await governReport();
229
+ },
230
+ );
231
+
232
+ it(
233
+ 'parses and summarizes audit log',
234
+ { timeout: 10_000 },
235
+ async () => {
236
+ const projectDir = join(tmpDir, 'with-log');
237
+ await mkdir(projectDir, { recursive: true });
238
+ process.chdir(projectDir);
239
+
240
+ const entries = [
241
+ { score: 90, passed: true, violationCount: 1 },
242
+ { score: 70, passed: false, violationCount: 5 },
243
+ { score: 100, passed: true, violationCount: 0 },
244
+ ];
245
+ const logContent = entries
246
+ .map((e) => JSON.stringify(e))
247
+ .join('\n');
248
+ await writeFile(join(projectDir, '.govern-audit.jsonl'), logContent);
249
+
250
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
251
+ const { governReport } = await import('../govern.js');
252
+ await governReport();
253
+ consoleSpy.mockRestore();
254
+
255
+ // Verify it ran without error — the console output is tested implicitly
256
+ },
257
+ );
258
+ });
@@ -16,7 +16,7 @@ describe("init", () => {
16
16
  await rm(tmpDir, { recursive: true, force: true });
17
17
  });
18
18
 
19
- it("skips Fragments UI runtime injection for shadcn-style projects", async () => {
19
+ it("skips Fragments UI runtime injection for shadcn-style projects", { timeout: 10_000 }, async () => {
20
20
  const projectDir = join(tmpDir, "shadcn-app");
21
21
  await mkdir(join(projectDir, "src", "components", "ui"), { recursive: true });
22
22
 
@@ -15,7 +15,7 @@ import {
15
15
  buildEnrichedUsageBlock,
16
16
  type EnrichmentResult,
17
17
  } from "../scan-generate.js";
18
- import type { ComponentMeta } from "../../core/component-extractor.js";
18
+ import type { ComponentMeta } from '@fragments-sdk/extract';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
21
  // Helper to build a minimal ComponentMeta for tests
@@ -5,7 +5,7 @@
5
5
  import pc from 'picocolors';
6
6
  import { BRAND } from '../core/index.js';
7
7
  import { loadConfig } from '../core/node.js';
8
- import { buildFragments, buildFragmentsDir } from '../build.js';
8
+ import { buildFragments, buildFragmentsDir, getFragmentsJsonStatus } from '@fragments-sdk/compiler';
9
9
  import { scan } from './scan.js';
10
10
 
11
11
  /**
@@ -22,6 +22,10 @@ export interface BuildOptions {
22
22
  registryOnly?: boolean;
23
23
  /** Build from source code (zero-config, no fragment files needed) */
24
24
  fromSource?: boolean;
25
+ /** Skip work when fragments.json is already fresh */
26
+ ifNeeded?: boolean;
27
+ /** Check freshness and exit non-zero if fragments.json needs regeneration */
28
+ check?: boolean;
25
29
  /** Skip usage analysis when building from source */
26
30
  skipUsage?: boolean;
27
31
  /** Skip Storybook parsing when building from source */
@@ -88,6 +92,55 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
88
92
 
89
93
  // Build fragments.json unless --registry-only
90
94
  if (!options.registryOnly) {
95
+ const status = await getFragmentsJsonStatus(config, configDir, {
96
+ output: config.outFile,
97
+ configPath: options.config,
98
+ });
99
+
100
+ if (options.check) {
101
+ if (status.missing || status.stale) {
102
+ const message = status.missing
103
+ ? `${BRAND.outFile} is missing`
104
+ : `${BRAND.outFile} is stale${status.reason ? ` (${status.reason})` : ''}`;
105
+ console.log(pc.red(`✗ ${message}\n`));
106
+ errors.push({
107
+ file: status.outputPath,
108
+ error: message,
109
+ });
110
+ return {
111
+ success: false,
112
+ outputPath: status.outputPath,
113
+ errors,
114
+ };
115
+ }
116
+
117
+ console.log(pc.green(`✓ ${BRAND.outFile} is up to date`));
118
+ console.log(pc.dim(` Output: ${status.outputPath}\n`));
119
+
120
+ if (!options.registry) {
121
+ return {
122
+ success: true,
123
+ fragmentCount: Object.keys(status.data?.fragments ?? {}).length,
124
+ outputPath: status.outputPath,
125
+ errors,
126
+ };
127
+ }
128
+ }
129
+
130
+ if (options.ifNeeded && !status.missing && !status.stale) {
131
+ console.log(pc.green(`✓ ${BRAND.outFile} is up to date`));
132
+ console.log(pc.dim(` Output: ${status.outputPath}\n`));
133
+
134
+ if (!options.registry) {
135
+ return {
136
+ success: true,
137
+ fragmentCount: Object.keys(status.data?.fragments ?? {}).length,
138
+ outputPath: status.outputPath,
139
+ errors,
140
+ };
141
+ }
142
+ }
143
+
91
144
  console.log(pc.dim('Compiling fragments...\n'));
92
145
 
93
146
  const result = await buildFragments(config, configDir);
@@ -8,7 +8,7 @@ import pc from 'picocolors';
8
8
  import { generateContext } from '../core/index.js';
9
9
  import type { CompiledFragment, CompiledFragmentsFile } from '../core/index.js';
10
10
  import { loadConfig } from '../core/node.js';
11
- import { buildFragments } from '../build.js';
11
+ import { buildFragments } from '@fragments-sdk/compiler';
12
12
 
13
13
  /**
14
14
  * Options for context command