@fragments-sdk/cli 0.15.0 → 0.15.2

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 (120) hide show
  1. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  2. package/dist/bin.js +565 -548
  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-JVAU3YKN.js +852 -0
  19. package/dist/create-JVAU3YKN.js.map +1 -0
  20. package/dist/doctor-BDPMYYE6.js +385 -0
  21. package/dist/doctor-BDPMYYE6.js.map +1 -0
  22. package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
  23. package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
  24. package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
  25. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  26. package/dist/index.d.ts +2 -22
  27. package/dist/index.js +8 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
  30. package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
  31. package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
  32. package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  33. package/dist/mcp-bin.js +1 -2
  34. package/dist/mcp-bin.js.map +1 -1
  35. package/dist/node-37AUE74M.js +65 -0
  36. package/dist/push-contracts-WY32TFP6.js +84 -0
  37. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  38. package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
  39. package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
  40. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  41. package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
  42. package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
  43. package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
  44. package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  45. package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
  46. package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
  47. package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
  48. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  49. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  50. package/dist/token-parser-32KOIOFN.js +22 -0
  51. package/dist/token-parser-32KOIOFN.js.map +1 -0
  52. package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
  53. package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
  54. package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
  55. package/dist/tokens-push-HY3KO36V.js +148 -0
  56. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  57. package/package.json +18 -16
  58. package/src/bin.ts +94 -1
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  61. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  62. package/src/commands/__tests__/create.test.ts +71 -0
  63. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  64. package/src/commands/__tests__/govern.test.ts +258 -0
  65. package/src/commands/__tests__/init.test.ts +9 -1
  66. package/src/commands/__tests__/scan-generate.test.ts +1 -1
  67. package/src/commands/build.ts +54 -1
  68. package/src/commands/context.ts +1 -1
  69. package/src/commands/create.ts +590 -0
  70. package/src/commands/doctor.ts +3 -2
  71. package/src/commands/govern-scan.ts +187 -8
  72. package/src/commands/govern.ts +65 -2
  73. package/src/commands/init-cloud.ts +32 -4
  74. package/src/commands/push-contracts.ts +112 -0
  75. package/src/commands/scan-generate.ts +1 -1
  76. package/src/commands/scan.ts +13 -0
  77. package/src/commands/sync.ts +2 -2
  78. package/src/commands/tokens-push.ts +199 -0
  79. package/src/core/__tests__/token-resolver.test.ts +1 -1
  80. package/src/core/component-extractor.test.ts +1 -1
  81. package/src/core/drift-verifier.ts +1 -1
  82. package/src/core/extractor-adapter.ts +1 -1
  83. package/src/index.ts +3 -3
  84. package/src/migrate/fragment-to-contract.ts +2 -2
  85. package/src/service/index.ts +8 -0
  86. package/src/service/tailwind-v4-parser.ts +314 -0
  87. package/src/service/token-parser.ts +56 -0
  88. package/src/setup.ts +10 -39
  89. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  90. package/src/theme/__tests__/serializer.test.ts +1 -1
  91. package/src/theme/generator.ts +30 -1
  92. package/src/theme/schema.ts +8 -0
  93. package/src/theme/serializer.ts +13 -9
  94. package/src/theme/types.ts +8 -0
  95. package/src/validators.ts +1 -2
  96. package/dist/chunk-65WSVDV5.js.map +0 -1
  97. package/dist/chunk-7WHVW72L.js +0 -2664
  98. package/dist/chunk-7WHVW72L.js.map +0 -1
  99. package/dist/chunk-CZD3AD4Q.js.map +0 -1
  100. package/dist/chunk-MN3TJ3D5.js +0 -695
  101. package/dist/chunk-MN3TJ3D5.js.map +0 -1
  102. package/dist/chunk-XJQ5BIWI.js.map +0 -1
  103. package/dist/chunk-Z7EY4VHE.js +0 -50
  104. package/dist/govern-scan-UCBZR6D6.js.map +0 -1
  105. package/dist/sass.node-4XJK6YBF.js +0 -130708
  106. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  107. package/dist/scan-generate-TWRHNU5M.js.map +0 -1
  108. package/src/build.ts +0 -736
  109. package/src/core/auto-props.ts +0 -464
  110. package/src/core/component-extractor.ts +0 -1121
  111. package/src/core/token-resolver.ts +0 -155
  112. package/src/viewer/preview-adapter.ts +0 -116
  113. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  114. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  115. /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
  116. /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
  117. /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
  118. /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
  119. /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
  120. /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
+ });
@@ -13,7 +13,15 @@ describe("init", () => {
13
13
  });
14
14
 
15
15
  afterAll(async () => {
16
- await rm(tmpDir, { recursive: true, force: true });
16
+ // Retry cleanup CI runners can race with async handles
17
+ for (let i = 0; i < 3; i++) {
18
+ try {
19
+ await rm(tmpDir, { recursive: true, force: true });
20
+ break;
21
+ } catch {
22
+ await new Promise((r) => setTimeout(r, 200));
23
+ }
24
+ }
17
25
  });
18
26
 
19
27
  it("skips Fragments UI runtime injection for shadcn-style projects", async () => {
@@ -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