@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.
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +565 -548
- package/dist/bin.js.map +1 -1
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
- package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
- package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
- package/dist/core/index.js +0 -1
- package/dist/create-JVAU3YKN.js +852 -0
- package/dist/create-JVAU3YKN.js.map +1 -0
- package/dist/doctor-BDPMYYE6.js +385 -0
- package/dist/doctor-BDPMYYE6.js.map +1 -0
- package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
- package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +2 -22
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
- package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
- package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
- package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +1 -2
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
- package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
- package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
- package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
- package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
- package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
- package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
- package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
- package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +18 -16
- package/src/bin.ts +94 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +9 -1
- package/src/commands/__tests__/scan-generate.test.ts +1 -1
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +590 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/govern-scan.ts +187 -8
- package/src/commands/govern.ts +65 -2
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +1 -1
- package/src/commands/scan.ts +13 -0
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-push.ts +199 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +1 -1
- package/src/core/drift-verifier.ts +1 -1
- package/src/core/extractor-adapter.ts +1 -1
- package/src/index.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +2 -2
- package/src/service/index.ts +8 -0
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +30 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/dist/chunk-65WSVDV5.js.map +0 -1
- package/dist/chunk-7WHVW72L.js +0 -2664
- package/dist/chunk-7WHVW72L.js.map +0 -1
- package/dist/chunk-CZD3AD4Q.js.map +0 -1
- package/dist/chunk-MN3TJ3D5.js +0 -695
- package/dist/chunk-MN3TJ3D5.js.map +0 -1
- package/dist/chunk-XJQ5BIWI.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/govern-scan-UCBZR6D6.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-generate-TWRHNU5M.js.map +0 -1
- package/src/build.ts +0 -736
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1121
- package/src/core/token-resolver.ts +0 -155
- package/src/viewer/preview-adapter.ts +0 -116
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
- /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
- /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
|
@@ -90,6 +90,7 @@ export async function governScan(
|
|
|
90
90
|
buildAdaptersFromConfig,
|
|
91
91
|
createCloudAdapter,
|
|
92
92
|
formatVerdict,
|
|
93
|
+
computeComponentHealth,
|
|
93
94
|
} = await import('@fragments-sdk/govern');
|
|
94
95
|
|
|
95
96
|
const { scanCodebase } = await import(
|
|
@@ -123,20 +124,65 @@ export async function governScan(
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
// 3.
|
|
127
|
+
// 3. Extract code tokens for cloud ingest
|
|
128
|
+
let codeTokens: string | undefined;
|
|
129
|
+
if (process.env.FRAGMENTS_API_KEY) {
|
|
130
|
+
codeTokens = await extractCodeTokens(rootDir, options.config, quiet);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3b. Load contract registry for governance + cloud ingest (if fragments.json exists)
|
|
134
|
+
let contractRegistry: string | undefined;
|
|
135
|
+
let registryMap: Record<string, unknown> | undefined;
|
|
136
|
+
{
|
|
137
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
138
|
+
const fragmentsJsonPath = resolve(rootDir, 'fragments.json');
|
|
139
|
+
if (existsSync(fragmentsJsonPath)) {
|
|
140
|
+
try {
|
|
141
|
+
const raw = readFileSync(fragmentsJsonPath, 'utf-8');
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
if (parsed.fragments && Array.isArray(parsed.fragments)) {
|
|
144
|
+
// Build name-keyed map for engine registry injection
|
|
145
|
+
const map: Record<string, unknown> = {};
|
|
146
|
+
for (const f of parsed.fragments) {
|
|
147
|
+
if (f.meta?.name) {
|
|
148
|
+
map[f.meta.name] = f;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
registryMap = map;
|
|
152
|
+
|
|
153
|
+
if (process.env.FRAGMENTS_API_KEY) {
|
|
154
|
+
contractRegistry = JSON.stringify({ fragments: parsed.fragments });
|
|
155
|
+
}
|
|
156
|
+
if (!quiet) {
|
|
157
|
+
console.log(pc.dim(` Contract registry loaded (${parsed.fragments.length} components)\n`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Invalid fragments.json — skip
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 4. Build adapters. Auto-add cloud if FRAGMENTS_API_KEY is set
|
|
127
167
|
const adapters = buildAdaptersFromConfig(policy.audit);
|
|
128
168
|
const hasCloudAdapter = adapters.length > 0 && policy.audit?.cloud;
|
|
129
169
|
if (!hasCloudAdapter && process.env.FRAGMENTS_API_KEY) {
|
|
130
|
-
adapters.push(createCloudAdapter());
|
|
170
|
+
adapters.push(createCloudAdapter({ codeTokens, contractRegistry }));
|
|
131
171
|
if (!quiet) {
|
|
132
172
|
console.log(pc.dim(' Cloud audit enabled (FRAGMENTS_API_KEY detected)\n'));
|
|
133
173
|
}
|
|
134
174
|
}
|
|
135
175
|
|
|
136
|
-
//
|
|
137
|
-
const engine = createEngine(
|
|
176
|
+
// 5. Create engine (with registry for contract-aware validators)
|
|
177
|
+
const engine = createEngine(
|
|
178
|
+
policy,
|
|
179
|
+
adapters,
|
|
180
|
+
registryMap
|
|
181
|
+
? { registry: { fragments: registryMap as Record<string, Record<string, unknown>> } }
|
|
182
|
+
: undefined,
|
|
183
|
+
);
|
|
138
184
|
|
|
139
|
-
//
|
|
185
|
+
// 6. Scan codebase
|
|
140
186
|
if (!quiet) {
|
|
141
187
|
console.log(pc.dim(' Scanning files...\n'));
|
|
142
188
|
}
|
|
@@ -163,7 +209,7 @@ export async function governScan(
|
|
|
163
209
|
);
|
|
164
210
|
}
|
|
165
211
|
|
|
166
|
-
//
|
|
212
|
+
// 7. Collect all usages across components
|
|
167
213
|
const allUsages: ComponentUsage[] = [];
|
|
168
214
|
for (const comp of Object.values(analysis.components)) {
|
|
169
215
|
allUsages.push(...comp.usages);
|
|
@@ -176,12 +222,23 @@ export async function governScan(
|
|
|
176
222
|
return { exitCode: 0 };
|
|
177
223
|
}
|
|
178
224
|
|
|
179
|
-
//
|
|
225
|
+
// 8. Group by file and run checks
|
|
180
226
|
const grouped = groupByFile(allUsages);
|
|
181
227
|
let totalFiles = 0;
|
|
182
228
|
let passedFiles = 0;
|
|
183
229
|
let totalViolations = 0;
|
|
184
230
|
const violationCounts = new Map<string, number>();
|
|
231
|
+
const allVerdicts: Awaited<ReturnType<typeof engine.check>>[] = [];
|
|
232
|
+
|
|
233
|
+
// Build per-file usage snapshot for cloud
|
|
234
|
+
const usageSnapshot: Array<{
|
|
235
|
+
file: string;
|
|
236
|
+
components: Array<{
|
|
237
|
+
name: string;
|
|
238
|
+
line: number;
|
|
239
|
+
props: { static: Record<string, unknown>; dynamic: string[] };
|
|
240
|
+
}>;
|
|
241
|
+
}> = [];
|
|
185
242
|
|
|
186
243
|
for (const [filePath, usages] of grouped) {
|
|
187
244
|
const spec = usagesToSpec(usages, filePath, rootDir);
|
|
@@ -191,6 +248,20 @@ export async function governScan(
|
|
|
191
248
|
runner: 'cli',
|
|
192
249
|
input: relPath,
|
|
193
250
|
});
|
|
251
|
+
allVerdicts.push(verdict);
|
|
252
|
+
|
|
253
|
+
// Collect per-file usage snapshot
|
|
254
|
+
usageSnapshot.push({
|
|
255
|
+
file: relPath,
|
|
256
|
+
components: usages.map((u) => ({
|
|
257
|
+
name: u.componentName,
|
|
258
|
+
line: u.line,
|
|
259
|
+
props: {
|
|
260
|
+
static: u.props.static,
|
|
261
|
+
dynamic: u.props.dynamic,
|
|
262
|
+
},
|
|
263
|
+
})),
|
|
264
|
+
});
|
|
194
265
|
|
|
195
266
|
totalFiles++;
|
|
196
267
|
|
|
@@ -230,7 +301,10 @@ export async function governScan(
|
|
|
230
301
|
}
|
|
231
302
|
}
|
|
232
303
|
|
|
233
|
-
//
|
|
304
|
+
// 8b. Compute component health
|
|
305
|
+
const health = computeComponentHealth(allVerdicts, registryMap ?? {});
|
|
306
|
+
|
|
307
|
+
// 9. Summary
|
|
234
308
|
if (!quiet && format === 'summary') {
|
|
235
309
|
console.log(pc.dim('\n ─────────────────────────────────────\n'));
|
|
236
310
|
console.log(` Files checked: ${totalFiles}`);
|
|
@@ -245,6 +319,15 @@ export async function governScan(
|
|
|
245
319
|
}
|
|
246
320
|
}
|
|
247
321
|
|
|
322
|
+
// Component health
|
|
323
|
+
console.log(pc.dim('\n Component Health:'));
|
|
324
|
+
console.log(` Contract coverage: ${health.contractCoverage}% (${health.contractedComponents}/${health.totalComponents})`);
|
|
325
|
+
console.log(` Compliance rate: ${health.overallCompliance}%`);
|
|
326
|
+
|
|
327
|
+
if (health.uncontracted.length > 0) {
|
|
328
|
+
console.log(pc.dim(` Uncontracted: ${health.uncontracted.slice(0, 5).join(', ')}${health.uncontracted.length > 5 ? ` (+${health.uncontracted.length - 5} more)` : ''}`));
|
|
329
|
+
}
|
|
330
|
+
|
|
248
331
|
console.log();
|
|
249
332
|
|
|
250
333
|
if (passedFiles === totalFiles) {
|
|
@@ -256,9 +339,105 @@ export async function governScan(
|
|
|
256
339
|
}
|
|
257
340
|
}
|
|
258
341
|
|
|
342
|
+
// 10. Push component usage snapshot + health to Cloud (if API key set)
|
|
343
|
+
if (process.env.FRAGMENTS_API_KEY && usageSnapshot.length > 0) {
|
|
344
|
+
try {
|
|
345
|
+
const apiKey = process.env.FRAGMENTS_API_KEY;
|
|
346
|
+
const url = process.env.FRAGMENTS_URL ?? 'https://app.usefragments.com';
|
|
347
|
+
await fetch(`${url}/api/ingest`, {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: {
|
|
350
|
+
'Content-Type': 'application/json',
|
|
351
|
+
Authorization: `Bearer ${apiKey}`,
|
|
352
|
+
},
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
componentUsage: JSON.stringify(usageSnapshot),
|
|
355
|
+
componentHealth: JSON.stringify(health),
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
} catch {
|
|
359
|
+
// Non-critical — don't fail the scan
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
259
363
|
return { exitCode: passedFiles === totalFiles ? 0 : 1 };
|
|
260
364
|
}
|
|
261
365
|
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Token extraction for cloud ingest
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Auto-detect and extract code tokens to send alongside governance verdicts.
|
|
372
|
+
* Tries: 1) tokens config from fragments.config.ts, 2) Tailwind config.
|
|
373
|
+
* Returns a flat JSON string of token name → value, or undefined if nothing found.
|
|
374
|
+
*/
|
|
375
|
+
async function extractCodeTokens(
|
|
376
|
+
rootDir: string,
|
|
377
|
+
configPath?: string,
|
|
378
|
+
quiet?: boolean,
|
|
379
|
+
): Promise<string | undefined> {
|
|
380
|
+
try {
|
|
381
|
+
// 1. Try fragments.config.ts tokens config
|
|
382
|
+
try {
|
|
383
|
+
const { loadConfig } = await import('../core/node.js');
|
|
384
|
+
const { parseTokenFiles } = await import('../service/index.js');
|
|
385
|
+
|
|
386
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
387
|
+
if (config.tokens?.include?.length) {
|
|
388
|
+
const result = await parseTokenFiles(config.tokens, configDir);
|
|
389
|
+
if (result.tokens.length > 0) {
|
|
390
|
+
const flat: Record<string, string> = {};
|
|
391
|
+
for (const token of result.tokens) {
|
|
392
|
+
flat[token.name] = token.resolvedValue;
|
|
393
|
+
}
|
|
394
|
+
if (!quiet) {
|
|
395
|
+
console.log(
|
|
396
|
+
pc.dim(` Extracted ${result.tokens.length} code tokens from config\n`),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return JSON.stringify(flat);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
// No config or no tokens section — fall through
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 2. Try Tailwind config
|
|
407
|
+
const {
|
|
408
|
+
findTailwindConfig,
|
|
409
|
+
loadTailwindConfig,
|
|
410
|
+
} = await import('../service/token-normalizer.js');
|
|
411
|
+
|
|
412
|
+
const tailwindPath = findTailwindConfig(rootDir);
|
|
413
|
+
if (tailwindPath) {
|
|
414
|
+
const tokens = await loadTailwindConfig(tailwindPath);
|
|
415
|
+
if (tokens.length > 0) {
|
|
416
|
+
const flat: Record<string, string> = {};
|
|
417
|
+
for (const token of tokens) {
|
|
418
|
+
flat[token.name] = token.value;
|
|
419
|
+
}
|
|
420
|
+
if (!quiet) {
|
|
421
|
+
console.log(
|
|
422
|
+
pc.dim(` Extracted ${tokens.length} tokens from Tailwind config\n`),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return JSON.stringify(flat);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (!quiet) {
|
|
430
|
+
console.log(
|
|
431
|
+
pc.dim(
|
|
432
|
+
` Token extraction skipped: ${error instanceof Error ? error.message : 'unknown error'}\n`,
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
262
441
|
// ---------------------------------------------------------------------------
|
|
263
442
|
// governWatch
|
|
264
443
|
// ---------------------------------------------------------------------------
|
package/src/commands/govern.ts
CHANGED
|
@@ -62,14 +62,77 @@ export async function governCheck(options: GovernCheckOptions = {}): Promise<{ e
|
|
|
62
62
|
|
|
63
63
|
export async function governInit(options: GovernInitOptions = {}): Promise<void> {
|
|
64
64
|
const { writeFile } = await import('node:fs/promises');
|
|
65
|
+
const { existsSync } = await import('node:fs');
|
|
65
66
|
const { resolve } = await import('node:path');
|
|
66
67
|
const { generateConfigTemplate } = await import('@fragments-sdk/govern');
|
|
68
|
+
const { findTailwindConfig } = await import(
|
|
69
|
+
'../service/token-normalizer.js'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
|
|
74
|
+
// Auto-detect token sources
|
|
75
|
+
const detected: { tokenIncludes: string[]; projectType: string } = {
|
|
76
|
+
tokenIncludes: [],
|
|
77
|
+
projectType: 'unknown',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Check for Tailwind
|
|
81
|
+
const tailwindPath = findTailwindConfig(cwd);
|
|
82
|
+
if (tailwindPath) {
|
|
83
|
+
const { relative } = await import('node:path');
|
|
84
|
+
detected.tokenIncludes.push(relative(cwd, tailwindPath));
|
|
85
|
+
detected.projectType = 'tailwind';
|
|
86
|
+
console.log(pc.dim(` Detected Tailwind config: ${detected.tokenIncludes[0]}`));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check for common SCSS/CSS token files
|
|
90
|
+
if (detected.tokenIncludes.length === 0) {
|
|
91
|
+
const candidates = [
|
|
92
|
+
'src/styles/tokens.scss',
|
|
93
|
+
'src/styles/variables.scss',
|
|
94
|
+
'src/styles/theme.scss',
|
|
95
|
+
'src/tokens.css',
|
|
96
|
+
'src/styles/tokens.css',
|
|
97
|
+
'src/styles/variables.css',
|
|
98
|
+
'styles/tokens.scss',
|
|
99
|
+
'styles/variables.scss',
|
|
100
|
+
'tokens.json',
|
|
101
|
+
'src/tokens.json',
|
|
102
|
+
];
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
if (existsSync(resolve(cwd, candidate))) {
|
|
105
|
+
detected.tokenIncludes.push(candidate);
|
|
106
|
+
detected.projectType = candidate.endsWith('.scss')
|
|
107
|
+
? 'scss'
|
|
108
|
+
: candidate.endsWith('.json')
|
|
109
|
+
? 'dtcg'
|
|
110
|
+
: 'css';
|
|
111
|
+
console.log(pc.dim(` Detected token source: ${candidate}`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (detected.tokenIncludes.length === 0) {
|
|
117
|
+
console.log(pc.dim(' No token sources auto-detected. You can add them manually.'));
|
|
118
|
+
}
|
|
67
119
|
|
|
68
120
|
const outputPath = resolve(options.output ?? 'fragments.config.ts');
|
|
69
|
-
const template = generateConfigTemplate();
|
|
121
|
+
const template = generateConfigTemplate({ tokenIncludes: detected.tokenIncludes });
|
|
70
122
|
|
|
71
123
|
await writeFile(outputPath, template, 'utf-8');
|
|
72
|
-
console.log(pc.green(
|
|
124
|
+
console.log(pc.green(`\n✓ Created ${outputPath}\n`));
|
|
125
|
+
|
|
126
|
+
if (detected.tokenIncludes.length > 0) {
|
|
127
|
+
console.log(
|
|
128
|
+
pc.dim(` Token sources configured: ${detected.tokenIncludes.join(', ')}`),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
console.log(
|
|
132
|
+
pc.dim(' Run ') +
|
|
133
|
+
pc.cyan('fragments govern scan') +
|
|
134
|
+
pc.dim(' to check your project.\n'),
|
|
135
|
+
);
|
|
73
136
|
}
|
|
74
137
|
|
|
75
138
|
// ---------------------------------------------------------------------------
|
|
@@ -327,7 +327,34 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
|
|
|
327
327
|
console.log(pc.green(` ✓ Created ${BRAND.configFile}`));
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
// ── 6.
|
|
330
|
+
// ── 6. Push contracts if available ──────────────────────────────────
|
|
331
|
+
const fragmentsJsonPath = resolve('fragments.json');
|
|
332
|
+
if (existsSync(fragmentsJsonPath)) {
|
|
333
|
+
console.log(pc.dim('\n Found fragments.json — pushing contracts to Cloud...'));
|
|
334
|
+
try {
|
|
335
|
+
const { pushContracts } = await import('@fragments-sdk/govern');
|
|
336
|
+
const raw = readFileSync(fragmentsJsonPath, 'utf-8');
|
|
337
|
+
const parsed = JSON.parse(raw);
|
|
338
|
+
if (parsed.fragments && Array.isArray(parsed.fragments)) {
|
|
339
|
+
const result = await pushContracts({
|
|
340
|
+
contractRegistry: JSON.stringify({ fragments: parsed.fragments }),
|
|
341
|
+
apiKey: auth.apiKey,
|
|
342
|
+
url: cloudUrl,
|
|
343
|
+
});
|
|
344
|
+
if (result.ok) {
|
|
345
|
+
console.log(pc.green(` ✓ Pushed ${parsed.fragments.length} component contracts`));
|
|
346
|
+
} else {
|
|
347
|
+
console.log(pc.yellow(` ⚠ Contract push failed: ${result.error}`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
console.log(pc.yellow(' ⚠ Could not push contracts'));
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
console.log(pc.dim('\n No fragments.json found — run `fragments scan` or `fragments build` to generate contracts'));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── 7. Run first check ────────────────────────────────────────────
|
|
331
358
|
if (!options.skipCheck) {
|
|
332
359
|
console.log(pc.dim('\n Running first governance check...\n'));
|
|
333
360
|
try {
|
|
@@ -346,9 +373,10 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
|
|
|
346
373
|
}
|
|
347
374
|
}
|
|
348
375
|
|
|
349
|
-
// ──
|
|
376
|
+
// ── 8. Done ───────────────────────────────────────────────────────
|
|
350
377
|
console.log(pc.green('\n ✓ All set!') + ' Your project is connected to Fragments Cloud.\n');
|
|
351
378
|
console.log(pc.dim(` Dashboard: ${cloudUrl}`));
|
|
352
|
-
console.log(pc.dim(' Run checks:
|
|
353
|
-
console.log(pc.dim('
|
|
379
|
+
console.log(pc.dim(' Run checks: fragments govern scan'));
|
|
380
|
+
console.log(pc.dim(' Push contracts: fragments govern push-contracts'));
|
|
381
|
+
console.log(pc.dim(' View config: fragments.config.ts\n'));
|
|
354
382
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments push-contracts
|
|
3
|
+
*
|
|
4
|
+
* Push compiled component contracts to Fragments Cloud.
|
|
5
|
+
* Reads fragments.json from the project root (or builds it from .contract.json files)
|
|
6
|
+
* and posts the contract registry to the Cloud ingest endpoint.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import pc from 'picocolors';
|
|
12
|
+
import { BRAND } from '../core/index.js';
|
|
13
|
+
|
|
14
|
+
export interface PushContractsOptions {
|
|
15
|
+
/** Path to fragments.json or directory containing .contract.json files */
|
|
16
|
+
input?: string;
|
|
17
|
+
/** Fragments Cloud URL */
|
|
18
|
+
url?: string;
|
|
19
|
+
/** API key (defaults to FRAGMENTS_API_KEY env var) */
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/** Suppress output */
|
|
22
|
+
quiet?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function pushContracts(
|
|
26
|
+
options: PushContractsOptions = {},
|
|
27
|
+
): Promise<{ exitCode: number }> {
|
|
28
|
+
const quiet = options.quiet ?? false;
|
|
29
|
+
const apiKey = options.apiKey ?? process.env.FRAGMENTS_API_KEY;
|
|
30
|
+
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
console.error(
|
|
33
|
+
pc.red('No API key found. Set FRAGMENTS_API_KEY or pass --api-key.'),
|
|
34
|
+
);
|
|
35
|
+
return { exitCode: 1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!quiet) {
|
|
39
|
+
console.log(pc.cyan(`\n${BRAND.name} Push Contracts\n`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 1. Find contract registry
|
|
43
|
+
const inputPath = options.input
|
|
44
|
+
? resolve(options.input)
|
|
45
|
+
: resolve('fragments.json');
|
|
46
|
+
|
|
47
|
+
if (!existsSync(inputPath)) {
|
|
48
|
+
console.error(
|
|
49
|
+
pc.red(`Contract registry not found at ${inputPath}`),
|
|
50
|
+
);
|
|
51
|
+
console.error(
|
|
52
|
+
pc.dim('Run `fragments build` or `fragments scan` to generate fragments.json first.'),
|
|
53
|
+
);
|
|
54
|
+
return { exitCode: 1 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Parse and validate
|
|
58
|
+
let contractRegistry: string;
|
|
59
|
+
let componentCount = 0;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const raw = readFileSync(inputPath, 'utf-8');
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
const fragments = parsed.fragments ?? parsed;
|
|
65
|
+
|
|
66
|
+
if (!Array.isArray(fragments)) {
|
|
67
|
+
console.error(pc.red('Invalid contract registry: expected fragments array'));
|
|
68
|
+
return { exitCode: 1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
componentCount = fragments.length;
|
|
72
|
+
contractRegistry = JSON.stringify({ fragments });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(
|
|
75
|
+
pc.red('Failed to parse contract registry:'),
|
|
76
|
+
error instanceof Error ? error.message : 'unknown error',
|
|
77
|
+
);
|
|
78
|
+
return { exitCode: 1 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (componentCount === 0) {
|
|
82
|
+
console.warn(pc.yellow('No components found in contract registry. Nothing to push.'));
|
|
83
|
+
return { exitCode: 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!quiet) {
|
|
87
|
+
console.log(pc.dim(` Found ${componentCount} component contracts\n`));
|
|
88
|
+
console.log(pc.dim(' Pushing to Fragments Cloud...\n'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Push to Cloud
|
|
92
|
+
const { pushContracts: push } = await import('@fragments-sdk/govern');
|
|
93
|
+
|
|
94
|
+
const result = await push({
|
|
95
|
+
contractRegistry,
|
|
96
|
+
url: options.url,
|
|
97
|
+
apiKey,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
console.error(pc.red(`Failed to push contracts: ${result.error}`));
|
|
102
|
+
return { exitCode: 1 };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!quiet) {
|
|
106
|
+
console.log(
|
|
107
|
+
pc.green(` ✓ Pushed ${componentCount} component contracts to Fragments Cloud\n`),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { exitCode: 0 };
|
|
112
|
+
}
|
package/src/commands/scan.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
loadConfig,
|
|
24
24
|
discoverAllComponents,
|
|
25
|
+
findConfigFile,
|
|
25
26
|
type DiscoveredComponent,
|
|
26
27
|
} from "../core/node.js";
|
|
27
28
|
import {
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
type PropsExtractionResult,
|
|
39
40
|
type ParsedStoryFile,
|
|
40
41
|
} from "../service/index.js";
|
|
42
|
+
import { getGeneratorVersion } from '@fragments-sdk/compiler';
|
|
41
43
|
|
|
42
44
|
export interface ScanOptions {
|
|
43
45
|
/** Path to config file */
|
|
@@ -271,10 +273,21 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
|
|
|
271
273
|
// Write output
|
|
272
274
|
const outputPath = resolve(configDir, outputFile);
|
|
273
275
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
276
|
+
const generatorVersion = await getGeneratorVersion();
|
|
277
|
+
const buildInputs = components
|
|
278
|
+
.map((component) => relative(configDir, component.sourcePath).split("\\").join("/"))
|
|
279
|
+
.sort();
|
|
280
|
+
const configPath = options.config ? resolve(process.cwd(), options.config) : findConfigFile(configDir);
|
|
281
|
+
if (configPath) {
|
|
282
|
+
buildInputs.push(relative(configDir, configPath).split("\\").join("/"));
|
|
283
|
+
buildInputs.sort();
|
|
284
|
+
}
|
|
274
285
|
|
|
275
286
|
const output: CompiledFragmentsFile = {
|
|
276
287
|
version: "1.0.0",
|
|
277
288
|
generatedAt: new Date().toISOString(),
|
|
289
|
+
generatorVersion,
|
|
290
|
+
buildInputs,
|
|
278
291
|
fragments,
|
|
279
292
|
};
|
|
280
293
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { BRAND } from '../core/index.js';
|
|
|
13
13
|
import { loadConfig } from '../core/node.js';
|
|
14
14
|
import { discoverFragmentFiles, loadFragmentFile } from '../core/node.js';
|
|
15
15
|
import { parseFragmentFile } from '../core/parser.js';
|
|
16
|
-
import { resolveComponentSourcePath } from '
|
|
17
|
-
import { createComponentExtractor, type PropMeta, type CompositionMeta } from '
|
|
16
|
+
import { resolveComponentSourcePath } from '@fragments-sdk/extract';
|
|
17
|
+
import { createComponentExtractor, type PropMeta, type CompositionMeta } from '@fragments-sdk/extract';
|
|
18
18
|
import type { FragmentsConfig } from '@fragments-sdk/core';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|