@fragments-sdk/cli 0.15.10 → 0.16.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.
- package/dist/bin.js +896 -787
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-6SQPP47U.js → chunk-77AAP6R6.js} +532 -31
- package/dist/chunk-77AAP6R6.js.map +1 -0
- package/dist/{chunk-5JF26E55.js → chunk-ACFVKMVZ.js} +11 -11
- package/dist/{chunk-BJE3425I.js → chunk-ACX7YWZW.js} +2 -2
- package/dist/{chunk-ONUP6Z4W.js → chunk-G6UVWMFU.js} +8 -8
- package/dist/{chunk-2WXKALIG.js → chunk-OZZ4SVZX.js} +2 -2
- package/dist/{chunk-32LIWN2P.js → chunk-SJFSG7QF.js} +582 -261
- package/dist/chunk-SJFSG7QF.js.map +1 -0
- package/dist/{chunk-HQ6A6DTV.js → chunk-XRADMHMV.js} +315 -1089
- package/dist/chunk-XRADMHMV.js.map +1 -0
- package/dist/core/index.js +53 -1
- package/dist/{create-EXURTBKK.js → create-3ZFYQB3T.js} +2 -2
- package/dist/{doctor-BDPMYYE6.js → doctor-4IDUM7HI.js} +2 -2
- package/dist/{generate-PVOLUAAC.js → generate-VNUUWVWQ.js} +4 -4
- package/dist/{govern-scan-DW4QUAYD.js → govern-scan-HTACKYPF.js} +158 -120
- package/dist/govern-scan-HTACKYPF.js.map +1 -0
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-SSGUSP7Z.js → init-PXFRAQ64.js} +5 -5
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-PKSYSTRR.js → scan-L4GWGEZX.js} +5 -6
- package/dist/{scan-generate-VY27PIOX.js → scan-generate-74EYSAGH.js} +4 -4
- package/dist/{service-QJGWUIVL.js → service-VELQHEWV.js} +12 -14
- package/dist/{snapshot-WIJMEIFT.js → snapshot-DT4B6DPR.js} +2 -2
- package/dist/{static-viewer-7QIBQZRC.js → static-viewer-E4OJWFDJ.js} +3 -3
- package/dist/{test-64Z5BKBA.js → test-QJY2QO4X.js} +3 -3
- package/dist/{token-normalizer-TEPOVBPV.js → token-normalizer-56H4242J.js} +2 -2
- package/dist/{tokens-NZWFQIAB.js → tokens-K6URXFPK.js} +7 -8
- package/dist/{tokens-NZWFQIAB.js.map → tokens-K6URXFPK.js.map} +1 -1
- package/dist/{tokens-generate-5JQSJ27E.js → tokens-generate-EL6IN536.js} +2 -2
- package/package.json +7 -6
- package/src/bin.ts +49 -88
- 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__/context-cloud.test.ts +291 -0
- package/src/commands/__tests__/govern-scan.test.ts +185 -0
- package/src/commands/__tests__/govern.test.ts +1 -0
- package/src/commands/context-cloud.ts +355 -0
- package/src/commands/govern-scan-report.ts +170 -0
- package/src/commands/govern-scan.ts +42 -147
- package/src/commands/govern.ts +0 -157
- package/dist/chunk-32LIWN2P.js.map +0 -1
- package/dist/chunk-6SQPP47U.js.map +0 -1
- package/dist/chunk-HQ6A6DTV.js.map +0 -1
- package/dist/chunk-MHIBEEW4.js +0 -511
- package/dist/chunk-MHIBEEW4.js.map +0 -1
- package/dist/govern-scan-DW4QUAYD.js.map +0 -1
- package/dist/init-cloud-3DNKPWFB.js +0 -304
- package/dist/init-cloud-3DNKPWFB.js.map +0 -1
- package/dist/node-37AUE74M.js +0 -65
- package/dist/push-contracts-WY32TFP6.js +0 -84
- package/dist/push-contracts-WY32TFP6.js.map +0 -1
- package/dist/static-viewer-7QIBQZRC.js.map +0 -1
- package/dist/token-parser-32KOIOFN.js +0 -22
- package/dist/token-parser-32KOIOFN.js.map +0 -1
- package/dist/tokens-push-HY3KO36V.js +0 -148
- package/dist/tokens-push-HY3KO36V.js.map +0 -1
- package/src/commands/init-cloud.ts +0 -382
- package/src/commands/push-contracts.ts +0 -112
- package/src/commands/tokens-push.ts +0 -199
- /package/dist/{chunk-5JF26E55.js.map → chunk-ACFVKMVZ.js.map} +0 -0
- /package/dist/{chunk-BJE3425I.js.map → chunk-ACX7YWZW.js.map} +0 -0
- /package/dist/{chunk-ONUP6Z4W.js.map → chunk-G6UVWMFU.js.map} +0 -0
- /package/dist/{chunk-2WXKALIG.js.map → chunk-OZZ4SVZX.js.map} +0 -0
- /package/dist/{create-EXURTBKK.js.map → create-3ZFYQB3T.js.map} +0 -0
- /package/dist/{doctor-BDPMYYE6.js.map → doctor-4IDUM7HI.js.map} +0 -0
- /package/dist/{generate-PVOLUAAC.js.map → generate-VNUUWVWQ.js.map} +0 -0
- /package/dist/{init-SSGUSP7Z.js.map → init-PXFRAQ64.js.map} +0 -0
- /package/dist/{node-37AUE74M.js.map → scan-L4GWGEZX.js.map} +0 -0
- /package/dist/{scan-generate-VY27PIOX.js.map → scan-generate-74EYSAGH.js.map} +0 -0
- /package/dist/{scan-PKSYSTRR.js.map → service-VELQHEWV.js.map} +0 -0
- /package/dist/{snapshot-WIJMEIFT.js.map → snapshot-DT4B6DPR.js.map} +0 -0
- /package/dist/{service-QJGWUIVL.js.map → static-viewer-E4OJWFDJ.js.map} +0 -0
- /package/dist/{test-64Z5BKBA.js.map → test-QJY2QO4X.js.map} +0 -0
- /package/dist/{token-normalizer-TEPOVBPV.js.map → token-normalizer-56H4242J.js.map} +0 -0
- /package/dist/{tokens-generate-5JQSJ27E.js.map → tokens-generate-EL6IN536.js.map} +0 -0
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses real JSX/TSX files via the existing codebase scanner, converts
|
|
5
5
|
* component usages to UISpec, and runs governance checks per file.
|
|
6
|
-
* Optionally submits results to Fragments Cloud.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import pc from 'picocolors';
|
|
@@ -11,6 +10,13 @@ import { resolve, relative } from 'node:path';
|
|
|
11
10
|
import { existsSync } from 'node:fs';
|
|
12
11
|
import { BRAND } from '../core/index.js';
|
|
13
12
|
import type { ComponentUsage } from '../service/enhance/types.js';
|
|
13
|
+
import {
|
|
14
|
+
aggregateVerdicts,
|
|
15
|
+
flattenComponentUsage,
|
|
16
|
+
buildComplianceSummary,
|
|
17
|
+
writeGovernScanReport,
|
|
18
|
+
type GovernScanReport,
|
|
19
|
+
} from './govern-scan-report.js';
|
|
14
20
|
|
|
15
21
|
// ---------------------------------------------------------------------------
|
|
16
22
|
// Options
|
|
@@ -23,6 +29,8 @@ export interface GovernScanOptions {
|
|
|
23
29
|
config?: string;
|
|
24
30
|
/** Output format */
|
|
25
31
|
format?: 'summary' | 'json' | 'sarif';
|
|
32
|
+
/** Write an aggregated machine-readable JSON report */
|
|
33
|
+
report?: string;
|
|
26
34
|
/** Suppress non-error output */
|
|
27
35
|
quiet?: boolean;
|
|
28
36
|
}
|
|
@@ -78,6 +86,7 @@ function groupByFile(usages: ComponentUsage[]): Map<string, ComponentUsage[]> {
|
|
|
78
86
|
return grouped;
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
|
|
81
90
|
// ---------------------------------------------------------------------------
|
|
82
91
|
// governScan
|
|
83
92
|
// ---------------------------------------------------------------------------
|
|
@@ -89,9 +98,9 @@ export async function governScan(
|
|
|
89
98
|
loadPolicy,
|
|
90
99
|
createEngine,
|
|
91
100
|
buildAdaptersFromConfig,
|
|
92
|
-
createCloudAdapter,
|
|
93
101
|
formatVerdict,
|
|
94
102
|
computeComponentHealth,
|
|
103
|
+
computeScore,
|
|
95
104
|
} = await import('@fragments-sdk/govern');
|
|
96
105
|
|
|
97
106
|
const { scanCodebase } = await import(
|
|
@@ -125,15 +134,9 @@ export async function governScan(
|
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
|
|
128
|
-
// 3.
|
|
129
|
-
let codeTokens: string | undefined;
|
|
130
|
-
if (process.env.FRAGMENTS_API_KEY) {
|
|
131
|
-
codeTokens = await extractCodeTokens(rootDir, options.config, quiet);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 3b. Load contract registry for governance + cloud ingest (if fragments.json exists)
|
|
135
|
-
let contractRegistry: string | undefined;
|
|
137
|
+
// 3. Load contract registry for contract-aware scoring (if fragments.json exists)
|
|
136
138
|
let registryMap: Record<string, unknown> | undefined;
|
|
139
|
+
let hasRegistry = false;
|
|
137
140
|
{
|
|
138
141
|
const { readFileSync, existsSync } = await import('node:fs');
|
|
139
142
|
const fragmentsJsonPath = resolve(rootDir, 'fragments.json');
|
|
@@ -142,37 +145,28 @@ export async function governScan(
|
|
|
142
145
|
const raw = readFileSync(fragmentsJsonPath, 'utf-8');
|
|
143
146
|
const parsed = JSON.parse(raw);
|
|
144
147
|
if (parsed.fragments && Array.isArray(parsed.fragments)) {
|
|
145
|
-
// Build name-keyed map for engine registry injection
|
|
146
148
|
const map: Record<string, unknown> = {};
|
|
147
|
-
for (const
|
|
148
|
-
if (
|
|
149
|
-
map[
|
|
149
|
+
for (const fragment of parsed.fragments) {
|
|
150
|
+
if (fragment.meta?.name) {
|
|
151
|
+
map[fragment.meta.name] = fragment;
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
registryMap = map;
|
|
153
|
-
|
|
154
|
-
if (process.env.FRAGMENTS_API_KEY) {
|
|
155
|
-
contractRegistry = JSON.stringify({ fragments: parsed.fragments });
|
|
156
|
-
}
|
|
155
|
+
hasRegistry = true;
|
|
157
156
|
if (!quiet) {
|
|
158
|
-
console.log(
|
|
157
|
+
console.log(
|
|
158
|
+
pc.dim(` Contract registry loaded (${parsed.fragments.length} components)\n`),
|
|
159
|
+
);
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
} catch {
|
|
162
|
-
// Invalid fragments.json — skip
|
|
163
|
+
// Invalid fragments.json — skip registry-aware summary
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
// 4. Build adapters
|
|
168
|
+
// 4. Build adapters from policy config
|
|
168
169
|
const adapters = buildAdaptersFromConfig(policy.audit);
|
|
169
|
-
const hasCloudAdapter = adapters.length > 0 && policy.audit?.cloud;
|
|
170
|
-
if (!hasCloudAdapter && process.env.FRAGMENTS_API_KEY) {
|
|
171
|
-
adapters.push(createCloudAdapter({ codeTokens, contractRegistry }));
|
|
172
|
-
if (!quiet) {
|
|
173
|
-
console.log(pc.dim(' Cloud audit enabled (FRAGMENTS_API_KEY detected)\n'));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
170
|
|
|
177
171
|
// 5. Create engine (with registry for contract-aware validators)
|
|
178
172
|
const engine = createEngine(
|
|
@@ -220,6 +214,16 @@ export async function governScan(
|
|
|
220
214
|
if (!quiet) {
|
|
221
215
|
console.log(pc.yellow(' No component usages found.\n'));
|
|
222
216
|
}
|
|
217
|
+
if (options.report) {
|
|
218
|
+
const report: GovernScanReport = {
|
|
219
|
+
verdict: aggregateVerdicts([], computeScore, 'ci'),
|
|
220
|
+
componentUsage: [],
|
|
221
|
+
};
|
|
222
|
+
await writeGovernScanReport(options.report, report);
|
|
223
|
+
if (!quiet) {
|
|
224
|
+
console.log(pc.dim(` Wrote governance report: ${resolve(options.report)}\n`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
223
227
|
return { exitCode: 0 };
|
|
224
228
|
}
|
|
225
229
|
|
|
@@ -231,16 +235,6 @@ export async function governScan(
|
|
|
231
235
|
const violationCounts = new Map<string, number>();
|
|
232
236
|
const allVerdicts: Awaited<ReturnType<typeof engine.check>>[] = [];
|
|
233
237
|
|
|
234
|
-
// Build per-file usage snapshot for cloud
|
|
235
|
-
const usageSnapshot: Array<{
|
|
236
|
-
file: string;
|
|
237
|
-
components: Array<{
|
|
238
|
-
name: string;
|
|
239
|
-
line: number;
|
|
240
|
-
props: { static: Record<string, unknown>; dynamic: string[] };
|
|
241
|
-
}>;
|
|
242
|
-
}> = [];
|
|
243
|
-
|
|
244
238
|
for (const [filePath, usages] of grouped) {
|
|
245
239
|
const spec = usagesToSpec(usages, filePath, rootDir);
|
|
246
240
|
const relPath = relative(rootDir, filePath);
|
|
@@ -251,19 +245,6 @@ export async function governScan(
|
|
|
251
245
|
});
|
|
252
246
|
allVerdicts.push(verdict);
|
|
253
247
|
|
|
254
|
-
// Collect per-file usage snapshot
|
|
255
|
-
usageSnapshot.push({
|
|
256
|
-
file: relPath,
|
|
257
|
-
components: usages.map((u) => ({
|
|
258
|
-
name: u.componentName,
|
|
259
|
-
line: u.line,
|
|
260
|
-
props: {
|
|
261
|
-
static: u.props.static,
|
|
262
|
-
dynamic: u.props.dynamic,
|
|
263
|
-
},
|
|
264
|
-
})),
|
|
265
|
-
});
|
|
266
|
-
|
|
267
248
|
totalFiles++;
|
|
268
249
|
|
|
269
250
|
if (verdict.passed) {
|
|
@@ -340,103 +321,21 @@ export async function governScan(
|
|
|
340
321
|
}
|
|
341
322
|
}
|
|
342
323
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
headers: {
|
|
351
|
-
'Content-Type': 'application/json',
|
|
352
|
-
Authorization: `Bearer ${apiKey}`,
|
|
353
|
-
},
|
|
354
|
-
body: JSON.stringify({
|
|
355
|
-
componentUsage: JSON.stringify(usageSnapshot),
|
|
356
|
-
componentHealth: JSON.stringify(health),
|
|
357
|
-
}),
|
|
358
|
-
});
|
|
359
|
-
} catch {
|
|
360
|
-
// Non-critical — don't fail the scan
|
|
324
|
+
if (options.report) {
|
|
325
|
+
const report: GovernScanReport = {
|
|
326
|
+
verdict: aggregateVerdicts(allVerdicts, computeScore, 'ci'),
|
|
327
|
+
componentUsage: flattenComponentUsage(allUsages, rootDir),
|
|
328
|
+
};
|
|
329
|
+
if (hasRegistry) {
|
|
330
|
+
report.complianceSummary = buildComplianceSummary(health);
|
|
361
331
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return { exitCode: passedFiles === totalFiles ? 0 : 1 };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ---------------------------------------------------------------------------
|
|
368
|
-
// Token extraction for cloud ingest
|
|
369
|
-
// ---------------------------------------------------------------------------
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Auto-detect and extract code tokens to send alongside governance verdicts.
|
|
373
|
-
* Tries: 1) tokens config from fragments.config.ts, 2) Tailwind config.
|
|
374
|
-
* Returns a flat JSON string of token name → value, or undefined if nothing found.
|
|
375
|
-
*/
|
|
376
|
-
async function extractCodeTokens(
|
|
377
|
-
rootDir: string,
|
|
378
|
-
configPath?: string,
|
|
379
|
-
quiet?: boolean,
|
|
380
|
-
): Promise<string | undefined> {
|
|
381
|
-
try {
|
|
382
|
-
// 1. Try fragments.config.ts tokens config
|
|
383
|
-
try {
|
|
384
|
-
const { loadConfig } = await import('../core/node.js');
|
|
385
|
-
const { parseTokenFiles } = await import('../service/index.js');
|
|
386
|
-
|
|
387
|
-
const { config, configDir } = await loadConfig(configPath);
|
|
388
|
-
if (config.tokens?.include?.length) {
|
|
389
|
-
const result = await parseTokenFiles(config.tokens, configDir);
|
|
390
|
-
if (result.tokens.length > 0) {
|
|
391
|
-
const flat: Record<string, string> = {};
|
|
392
|
-
for (const token of result.tokens) {
|
|
393
|
-
flat[token.name] = token.resolvedValue;
|
|
394
|
-
}
|
|
395
|
-
if (!quiet) {
|
|
396
|
-
console.log(
|
|
397
|
-
pc.dim(` Extracted ${result.tokens.length} code tokens from config\n`),
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
return JSON.stringify(flat);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
} catch {
|
|
404
|
-
// No config or no tokens section — fall through
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// 2. Try Tailwind config
|
|
408
|
-
const {
|
|
409
|
-
findTailwindConfig,
|
|
410
|
-
loadTailwindConfig,
|
|
411
|
-
} = await import('../service/token-normalizer.js');
|
|
412
|
-
|
|
413
|
-
const tailwindPath = findTailwindConfig(rootDir);
|
|
414
|
-
if (tailwindPath) {
|
|
415
|
-
const tokens = await loadTailwindConfig(tailwindPath);
|
|
416
|
-
if (tokens.length > 0) {
|
|
417
|
-
const flat: Record<string, string> = {};
|
|
418
|
-
for (const token of tokens) {
|
|
419
|
-
flat[token.name] = token.value;
|
|
420
|
-
}
|
|
421
|
-
if (!quiet) {
|
|
422
|
-
console.log(
|
|
423
|
-
pc.dim(` Extracted ${tokens.length} tokens from Tailwind config\n`),
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
return JSON.stringify(flat);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} catch (error) {
|
|
332
|
+
await writeGovernScanReport(options.report, report);
|
|
430
333
|
if (!quiet) {
|
|
431
|
-
console.log(
|
|
432
|
-
pc.dim(
|
|
433
|
-
` Token extraction skipped: ${error instanceof Error ? error.message : 'unknown error'}\n`,
|
|
434
|
-
),
|
|
435
|
-
);
|
|
334
|
+
console.log(pc.dim(` Wrote governance report: ${resolve(options.report)}\n`));
|
|
436
335
|
}
|
|
437
336
|
}
|
|
438
337
|
|
|
439
|
-
return
|
|
338
|
+
return { exitCode: passedFiles === totalFiles ? 0 : 1 };
|
|
440
339
|
}
|
|
441
340
|
|
|
442
341
|
// ---------------------------------------------------------------------------
|
|
@@ -450,7 +349,6 @@ export async function governWatch(
|
|
|
450
349
|
loadPolicy,
|
|
451
350
|
createEngine,
|
|
452
351
|
buildAdaptersFromConfig,
|
|
453
|
-
createCloudAdapter,
|
|
454
352
|
formatVerdict,
|
|
455
353
|
} = await import('@fragments-sdk/govern');
|
|
456
354
|
|
|
@@ -480,9 +378,6 @@ export async function governWatch(
|
|
|
480
378
|
policy = { ...policy, rules: SCAN_DEFAULT_RULES };
|
|
481
379
|
}
|
|
482
380
|
const adapters = buildAdaptersFromConfig(policy.audit);
|
|
483
|
-
if (!adapters.some(() => policy.audit?.cloud) && process.env.FRAGMENTS_API_KEY) {
|
|
484
|
-
adapters.push(createCloudAdapter());
|
|
485
|
-
}
|
|
486
381
|
const engine = createEngine(policy, adapters);
|
|
487
382
|
|
|
488
383
|
// 3. Watch for changes
|
package/src/commands/govern.ts
CHANGED
|
@@ -135,163 +135,6 @@ export async function governInit(options: GovernInitOptions = {}): Promise<void>
|
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// connect
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
export async function governConnect(): Promise<void> {
|
|
143
|
-
const { readFile, writeFile, appendFile } = await import('node:fs/promises');
|
|
144
|
-
const { existsSync } = await import('node:fs');
|
|
145
|
-
const { resolve } = await import('node:path');
|
|
146
|
-
const { platform } = await import('node:os');
|
|
147
|
-
const { exec } = await import('node:child_process');
|
|
148
|
-
const { password, confirm } = await import('@inquirer/prompts');
|
|
149
|
-
|
|
150
|
-
const cloudUrl = process.env.FRAGMENTS_URL ?? 'https://app.usefragments.com';
|
|
151
|
-
|
|
152
|
-
console.log(pc.cyan(`\n ${BRAND.name} — Connect to Cloud\n`));
|
|
153
|
-
console.log(
|
|
154
|
-
pc.dim(' This will connect your project to the Fragments dashboard\n') +
|
|
155
|
-
pc.dim(' for centralized audit tracking and team visibility.\n'),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// ── Step 1: Get API key ──────────────────────────────────────────────────
|
|
159
|
-
console.log(pc.bold(' Step 1 of 3: Get your API key\n'));
|
|
160
|
-
|
|
161
|
-
const dashboardUrl = `${cloudUrl}/api-keys`;
|
|
162
|
-
console.log(pc.dim(` → Opening the dashboard in your browser...`));
|
|
163
|
-
console.log(pc.dim(` Copy your API key from Settings → API Keys\n`));
|
|
164
|
-
|
|
165
|
-
// Open browser (best-effort)
|
|
166
|
-
const os = platform();
|
|
167
|
-
const openCmd = os === 'darwin'
|
|
168
|
-
? `open "${dashboardUrl}"`
|
|
169
|
-
: os === 'win32'
|
|
170
|
-
? `start "" "${dashboardUrl}"`
|
|
171
|
-
: `xdg-open "${dashboardUrl}"`;
|
|
172
|
-
exec(openCmd);
|
|
173
|
-
|
|
174
|
-
let apiKey: string;
|
|
175
|
-
let orgName: string;
|
|
176
|
-
|
|
177
|
-
// eslint-disable-next-line no-constant-condition
|
|
178
|
-
while (true) {
|
|
179
|
-
apiKey = await password({
|
|
180
|
-
message: 'Paste your API key:',
|
|
181
|
-
mask: '*',
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
if (!apiKey.trim()) {
|
|
185
|
-
console.log(pc.yellow('\n API key cannot be empty. Please try again.\n'));
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Verify key against cloud
|
|
190
|
-
console.log(pc.dim('\n Verifying...'));
|
|
191
|
-
try {
|
|
192
|
-
const response = await fetch(`${cloudUrl}/api/verify`, {
|
|
193
|
-
headers: { Authorization: `Bearer ${apiKey.trim()}` },
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
if (!response.ok) {
|
|
197
|
-
console.log(pc.red(`\n ✗ Invalid API key (HTTP ${response.status}). Please try again.\n`));
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const data = (await response.json()) as { valid: boolean; orgName?: string };
|
|
202
|
-
if (!data.valid) {
|
|
203
|
-
console.log(pc.red('\n ✗ API key not recognized. Please try again.\n'));
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
orgName = data.orgName ?? 'your organization';
|
|
208
|
-
console.log(pc.green(`\n ✓ Connected to "${orgName}" (verified)\n`));
|
|
209
|
-
break;
|
|
210
|
-
} catch (error) {
|
|
211
|
-
console.log(
|
|
212
|
-
pc.red('\n ✗ Could not reach the dashboard.'),
|
|
213
|
-
);
|
|
214
|
-
console.log(
|
|
215
|
-
pc.dim(` ${error instanceof Error ? error.message : 'Network error'}\n`),
|
|
216
|
-
);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── Step 2: Save configuration ──────────────────────────────────────────
|
|
222
|
-
console.log(pc.bold(' Step 2 of 3: Save configuration\n'));
|
|
223
|
-
|
|
224
|
-
const saveToEnv = await confirm({
|
|
225
|
-
message: 'Save API key to .env file?',
|
|
226
|
-
default: true,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (saveToEnv) {
|
|
230
|
-
const envPath = resolve('.env');
|
|
231
|
-
const envEntry = `FRAGMENTS_API_KEY=${apiKey.trim()}`;
|
|
232
|
-
|
|
233
|
-
if (existsSync(envPath)) {
|
|
234
|
-
const envContent = await readFile(envPath, 'utf-8');
|
|
235
|
-
if (envContent.includes('FRAGMENTS_API_KEY=')) {
|
|
236
|
-
// Replace existing entry
|
|
237
|
-
const updated = envContent.replace(
|
|
238
|
-
/^FRAGMENTS_API_KEY=.*$/m,
|
|
239
|
-
envEntry,
|
|
240
|
-
);
|
|
241
|
-
await writeFile(envPath, updated, 'utf-8');
|
|
242
|
-
console.log(pc.green(' ✓ Updated FRAGMENTS_API_KEY in .env'));
|
|
243
|
-
} else {
|
|
244
|
-
await appendFile(envPath, `\n${envEntry}\n`, 'utf-8');
|
|
245
|
-
console.log(pc.green(' ✓ Added FRAGMENTS_API_KEY to .env'));
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
await writeFile(envPath, `${envEntry}\n`, 'utf-8');
|
|
249
|
-
console.log(pc.green(' ✓ Created .env with FRAGMENTS_API_KEY'));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Write FRAGMENTS_URL only if non-default
|
|
253
|
-
if (cloudUrl !== 'https://app.usefragments.com') {
|
|
254
|
-
const envContent = await readFile(envPath, 'utf-8');
|
|
255
|
-
if (!envContent.includes('FRAGMENTS_URL=')) {
|
|
256
|
-
await appendFile(envPath, `FRAGMENTS_URL=${cloudUrl}\n`, 'utf-8');
|
|
257
|
-
console.log(pc.green(` ✓ Added FRAGMENTS_URL to .env`));
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Ensure .env is in .gitignore
|
|
262
|
-
const gitignorePath = resolve('.gitignore');
|
|
263
|
-
if (existsSync(gitignorePath)) {
|
|
264
|
-
const gitignore = await readFile(gitignorePath, 'utf-8');
|
|
265
|
-
if (!gitignore.split('\n').some((line) => line.trim() === '.env')) {
|
|
266
|
-
await appendFile(gitignorePath, '\n.env\n', 'utf-8');
|
|
267
|
-
console.log(pc.green(' ✓ Added .env to .gitignore'));
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
await writeFile(gitignorePath, '.env\n', 'utf-8');
|
|
271
|
-
console.log(pc.green(' ✓ Created .gitignore with .env entry'));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ── Step 3: Config check ────────────────────────────────────────────────
|
|
276
|
-
console.log(pc.bold('\n Step 3 of 3: Config check\n'));
|
|
277
|
-
|
|
278
|
-
const { findGovernConfig } = await import('@fragments-sdk/govern');
|
|
279
|
-
const configPath = findGovernConfig();
|
|
280
|
-
|
|
281
|
-
if (configPath) {
|
|
282
|
-
console.log(pc.green(` ✓ Found govern config: ${configPath}`));
|
|
283
|
-
} else {
|
|
284
|
-
console.log(
|
|
285
|
-
pc.yellow(' No govern config found — run `fragments govern init` to create one'),
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ── Done ────────────────────────────────────────────────────────────────
|
|
290
|
-
console.log(pc.dim('\n ─────────────────────────────────────\n'));
|
|
291
|
-
console.log(pc.green(' ✓ All set!') + ' Run `fragments govern check` to send your first audit.\n');
|
|
292
|
-
console.log(pc.dim(` Dashboard: ${cloudUrl}/overview\n`));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
138
|
// ---------------------------------------------------------------------------
|
|
296
139
|
// report
|
|
297
140
|
// ---------------------------------------------------------------------------
|