@fragments-sdk/cli 0.15.10 → 0.17.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 +901 -789
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-6SQPP47U.js → chunk-ANTWP3UG.js} +532 -31
- package/dist/chunk-ANTWP3UG.js.map +1 -0
- package/dist/{chunk-ONUP6Z4W.js → chunk-B4A4ZEGS.js} +9 -9
- package/dist/{chunk-32LIWN2P.js → chunk-FFCI6OVZ.js} +584 -261
- package/dist/chunk-FFCI6OVZ.js.map +1 -0
- package/dist/{chunk-HQ6A6DTV.js → chunk-HNHE64CR.js} +315 -1089
- package/dist/chunk-HNHE64CR.js.map +1 -0
- package/dist/{chunk-BJE3425I.js → chunk-MN3B2EE6.js} +2 -2
- package/dist/{chunk-QCN35LJU.js → chunk-SAQW37L5.js} +3 -2
- package/dist/chunk-SAQW37L5.js.map +1 -0
- package/dist/{chunk-2WXKALIG.js → chunk-SNZXGHL2.js} +2 -2
- package/dist/{chunk-5JF26E55.js → chunk-VT2J62ND.js} +11 -11
- package/dist/{codebase-scanner-MQHUZC2G.js → codebase-scanner-2T5QIDBA.js} +2 -2
- package/dist/core/index.js +53 -1
- package/dist/{create-EXURTBKK.js → create-D44QD7MV.js} +2 -2
- package/dist/{doctor-BDPMYYE6.js → doctor-7B5N4JYU.js} +2 -2
- package/dist/{generate-PVOLUAAC.js → generate-T47JZRVU.js} +4 -4
- package/dist/govern-scan-X6UEIOSV.js +632 -0
- package/dist/govern-scan-X6UEIOSV.js.map +1 -0
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/dist/{init-SSGUSP7Z.js → init-2RGAY4W6.js} +5 -5
- package/dist/mcp-bin.js +2 -2
- package/dist/scan-A2WJM54L.js +14 -0
- package/dist/{scan-generate-VY27PIOX.js → scan-generate-LUSOHT36.js} +4 -4
- package/dist/{service-QJGWUIVL.js → service-ROCP7TKG.js} +13 -15
- package/dist/{snapshot-WIJMEIFT.js → snapshot-B3SAW74Y.js} +2 -2
- package/dist/{static-viewer-7QIBQZRC.js → static-viewer-7L6UEYTJ.js} +3 -3
- package/dist/{test-64Z5BKBA.js → test-PQDVDURE.js} +3 -3
- package/dist/{token-normalizer-TEPOVBPV.js → token-normalizer-7TFCVDZL.js} +2 -2
- package/dist/{tokens-NZWFQIAB.js → tokens-64FG5FDP.js} +8 -9
- package/dist/{tokens-NZWFQIAB.js.map → tokens-64FG5FDP.js.map} +1 -1
- package/dist/{tokens-generate-5JQSJ27E.js → tokens-generate-CL4LBBQA.js} +2 -2
- package/package.json +9 -8
- package/src/bin.ts +55 -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 +282 -135
- package/src/commands/govern.ts +0 -157
- package/src/mcp/__tests__/server.integration.test.ts +9 -20
- package/src/service/enhance/codebase-scanner.ts +3 -2
- package/src/service/enhance/types.ts +3 -0
- 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/chunk-QCN35LJU.js.map +0 -1
- package/dist/govern-scan-DW4QUAYD.js +0 -414
- 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/scan-PKSYSTRR.js +0 -15
- 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-ONUP6Z4W.js.map → chunk-B4A4ZEGS.js.map} +0 -0
- /package/dist/{chunk-BJE3425I.js.map → chunk-MN3B2EE6.js.map} +0 -0
- /package/dist/{chunk-2WXKALIG.js.map → chunk-SNZXGHL2.js.map} +0 -0
- /package/dist/{chunk-5JF26E55.js.map → chunk-VT2J62ND.js.map} +0 -0
- /package/dist/{codebase-scanner-MQHUZC2G.js.map → codebase-scanner-2T5QIDBA.js.map} +0 -0
- /package/dist/{create-EXURTBKK.js.map → create-D44QD7MV.js.map} +0 -0
- /package/dist/{doctor-BDPMYYE6.js.map → doctor-7B5N4JYU.js.map} +0 -0
- /package/dist/{generate-PVOLUAAC.js.map → generate-T47JZRVU.js.map} +0 -0
- /package/dist/{init-SSGUSP7Z.js.map → init-2RGAY4W6.js.map} +0 -0
- /package/dist/{node-37AUE74M.js.map → scan-A2WJM54L.js.map} +0 -0
- /package/dist/{scan-generate-VY27PIOX.js.map → scan-generate-LUSOHT36.js.map} +0 -0
- /package/dist/{scan-PKSYSTRR.js.map → service-ROCP7TKG.js.map} +0 -0
- /package/dist/{snapshot-WIJMEIFT.js.map → snapshot-B3SAW74Y.js.map} +0 -0
- /package/dist/{service-QJGWUIVL.js.map → static-viewer-7L6UEYTJ.js.map} +0 -0
- /package/dist/{test-64Z5BKBA.js.map → test-PQDVDURE.js.map} +0 -0
- /package/dist/{token-normalizer-TEPOVBPV.js.map → token-normalizer-7TFCVDZL.js.map} +0 -0
- /package/dist/{tokens-generate-5JQSJ27E.js.map → tokens-generate-CL4LBBQA.js.map} +0 -0
package/src/bin.ts
CHANGED
|
@@ -23,6 +23,10 @@ const EXPERIMENTAL = process.env.FRAGMENTS_EXPERIMENTAL === '1';
|
|
|
23
23
|
import { validate } from './commands/validate.js';
|
|
24
24
|
import { build } from './commands/build.js';
|
|
25
25
|
import { context } from './commands/context.js';
|
|
26
|
+
import {
|
|
27
|
+
contextInstallCloud,
|
|
28
|
+
contextStatusCloud,
|
|
29
|
+
} from './commands/context-cloud.js';
|
|
26
30
|
import { list } from './commands/list.js';
|
|
27
31
|
import { reset } from './commands/reset.js';
|
|
28
32
|
import { compare } from './commands/compare.js';
|
|
@@ -45,7 +49,7 @@ import { perf } from './commands/perf.js';
|
|
|
45
49
|
type DoctorFn = typeof import('./commands/doctor.js')['doctor'];
|
|
46
50
|
import { setup } from './commands/setup.js';
|
|
47
51
|
import { sync } from './commands/sync.js';
|
|
48
|
-
import { governCheck, governInit, governReport
|
|
52
|
+
import { governCheck, governInit, governReport } from './commands/govern.js';
|
|
49
53
|
import { migrateContract } from './commands/migrate-contract.js';
|
|
50
54
|
|
|
51
55
|
// Import existing commands that were already extracted
|
|
@@ -157,8 +161,8 @@ program
|
|
|
157
161
|
// CONTEXT COMMAND
|
|
158
162
|
// ============================================================================
|
|
159
163
|
program
|
|
160
|
-
.command('context')
|
|
161
|
-
.description('Generate AI-ready context
|
|
164
|
+
.command('context [action]')
|
|
165
|
+
.description('Generate AI-ready context or install a Fragments Cloud bundle')
|
|
162
166
|
.option('-c, --config <path>', 'Path to config file')
|
|
163
167
|
.option('-i, --input <path>', `Path to ${BRAND.outFile} (builds if not provided)`)
|
|
164
168
|
.option('-f, --format <format>', 'Output format (markdown/json)', 'markdown')
|
|
@@ -166,8 +170,47 @@ program
|
|
|
166
170
|
.option('--code', 'Include code examples')
|
|
167
171
|
.option('--relations', 'Include component relationships')
|
|
168
172
|
.option('--tokens', 'Only output token estimate')
|
|
169
|
-
.
|
|
173
|
+
.option('--cloud', 'Use Fragments Cloud bundle endpoints')
|
|
174
|
+
.option('--api-key <key>', 'Fragments Cloud API key (or use FRAGMENTS_API_KEY)')
|
|
175
|
+
.option('--targets <list>', 'Comma-separated helper targets (cursor,agents,claude,copilot)')
|
|
176
|
+
.option('--cwd <path>', 'Project root for install/status')
|
|
177
|
+
.option('--dry-run', 'Preview file writes without modifying the repo')
|
|
178
|
+
.option('-y, --yes', 'Non-interactive mode')
|
|
179
|
+
.option('--root-files <mode>', 'Root instruction file mode (prompt|never|patch)')
|
|
180
|
+
.option('--gitignore-fragments', 'Add .fragments/ to .gitignore after install')
|
|
181
|
+
.action(async (action, options) => {
|
|
170
182
|
try {
|
|
183
|
+
if (options.cloud && action === 'install') {
|
|
184
|
+
await contextInstallCloud({
|
|
185
|
+
apiKey: options.apiKey,
|
|
186
|
+
targets: options.targets,
|
|
187
|
+
cwd: options.cwd,
|
|
188
|
+
dryRun: options.dryRun,
|
|
189
|
+
yes: options.yes,
|
|
190
|
+
rootFiles: options.rootFiles,
|
|
191
|
+
gitignoreFragments: options.gitignoreFragments,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (options.cloud && action === 'status') {
|
|
197
|
+
await contextStatusCloud({
|
|
198
|
+
apiKey: options.apiKey,
|
|
199
|
+
cwd: options.cwd,
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (action === 'install' || action === 'status') {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`context ${action} requires --cloud`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (action && action !== 'install' && action !== 'status') {
|
|
211
|
+
throw new Error(`Unknown context action: ${action}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
171
214
|
const result = await context({
|
|
172
215
|
config: options.config,
|
|
173
216
|
input: options.input,
|
|
@@ -885,35 +928,8 @@ const initCmd = program
|
|
|
885
928
|
.option('--api-key <key>', 'API key for AI enrichment')
|
|
886
929
|
.option('--model <model>', 'Override AI model for enrichment');
|
|
887
930
|
|
|
888
|
-
// Cloud governance flags — only visible with FRAGMENTS_EXPERIMENTAL=1
|
|
889
|
-
if (EXPERIMENTAL) {
|
|
890
|
-
initCmd
|
|
891
|
-
.option('--cloud', 'Set up Fragments Cloud governance (zero-config browser auth)')
|
|
892
|
-
.option('--cloud-url <url>', 'Cloud dashboard URL (default: https://app.usefragments.com)')
|
|
893
|
-
.option('--port <port>', 'Localhost port for auth callback (default: 9876)')
|
|
894
|
-
.option('--auth-only', 'Only authenticate, skip project setup')
|
|
895
|
-
.option('--skip-check', 'Skip running the first governance check');
|
|
896
|
-
}
|
|
897
|
-
|
|
898
931
|
initCmd.action(async (options) => {
|
|
899
932
|
try {
|
|
900
|
-
// Cloud init — experimental, requires FRAGMENTS_EXPERIMENTAL=1
|
|
901
|
-
if (options.cloud) {
|
|
902
|
-
if (!EXPERIMENTAL) {
|
|
903
|
-
console.log(pc.yellow(`\n Fragments Cloud is not yet publicly available.`));
|
|
904
|
-
console.log(pc.dim(` Set FRAGMENTS_EXPERIMENTAL=1 to enable preview features.\n`));
|
|
905
|
-
process.exit(1);
|
|
906
|
-
}
|
|
907
|
-
const { initCloud } = await import('./commands/init-cloud.js');
|
|
908
|
-
await initCloud({
|
|
909
|
-
url: options.cloudUrl,
|
|
910
|
-
port: options.port ? Number(options.port) : undefined,
|
|
911
|
-
authOnly: options.authOnly,
|
|
912
|
-
skipCheck: options.skipCheck,
|
|
913
|
-
});
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
933
|
const { init } = await import('./commands/init.js');
|
|
918
934
|
const result = await init({
|
|
919
935
|
projectRoot: process.cwd(),
|
|
@@ -1036,28 +1052,6 @@ tokensCmd
|
|
|
1036
1052
|
}
|
|
1037
1053
|
});
|
|
1038
1054
|
|
|
1039
|
-
tokensCmd
|
|
1040
|
-
.command('push')
|
|
1041
|
-
.description('Push code tokens to Fragments Cloud for drift comparison')
|
|
1042
|
-
.option('-c, --config <path>', 'Path to fragments config file')
|
|
1043
|
-
.option('--tailwind-v4 <path>', 'Path to Tailwind v4 CSS file with @theme block')
|
|
1044
|
-
.option('--dry-run', 'Parse and display tokens without pushing')
|
|
1045
|
-
.option('--verbose', 'Show detailed output')
|
|
1046
|
-
.action(async (options) => {
|
|
1047
|
-
try {
|
|
1048
|
-
const { tokensPush } = await import('./commands/tokens-push.js');
|
|
1049
|
-
await tokensPush({
|
|
1050
|
-
config: options.config,
|
|
1051
|
-
tailwindV4: options.tailwindV4,
|
|
1052
|
-
dryRun: options.dryRun,
|
|
1053
|
-
verbose: options.verbose,
|
|
1054
|
-
});
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1057
|
-
process.exit(1);
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
1055
|
// ============================================================================
|
|
1062
1056
|
// GENERATE COMMAND
|
|
1063
1057
|
// ============================================================================
|
|
@@ -1374,25 +1368,17 @@ governCmd
|
|
|
1374
1368
|
}
|
|
1375
1369
|
});
|
|
1376
1370
|
|
|
1377
|
-
governCmd
|
|
1378
|
-
.command('connect')
|
|
1379
|
-
.description('Connect your project to the Fragments Govern cloud dashboard')
|
|
1380
|
-
.action(async () => {
|
|
1381
|
-
try {
|
|
1382
|
-
await governConnect();
|
|
1383
|
-
} catch (error) {
|
|
1384
|
-
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1385
|
-
process.exit(1);
|
|
1386
|
-
}
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
1371
|
governCmd
|
|
1390
1372
|
.command('scan')
|
|
1391
1373
|
.description('Scan JSX/TSX codebase for governance violations')
|
|
1392
1374
|
.option('-d, --dir <path>', 'Root directory (default: auto-detect)')
|
|
1393
1375
|
.option('-c, --config <path>', 'Path to govern.config.ts')
|
|
1394
1376
|
.option('-f, --format <format>', 'Output format: summary, json, sarif', 'summary')
|
|
1377
|
+
.option('-r, --report <path>', 'Write an aggregated machine-readable JSON report')
|
|
1395
1378
|
.option('-q, --quiet', 'Suppress non-error output')
|
|
1379
|
+
.option('--api-key <key>', 'Fragments Cloud API key — report findings to Cloud')
|
|
1380
|
+
.option('--cloud-url <url>', 'Fragments Cloud URL (default: https://app.usefragments.com)')
|
|
1381
|
+
.option('--diff [base]', 'Only scan files changed vs a base ref (default: auto-detect merge base)')
|
|
1396
1382
|
.action(async (options) => {
|
|
1397
1383
|
try {
|
|
1398
1384
|
const { governScan } = await import('./commands/govern-scan.js');
|
|
@@ -1400,7 +1386,11 @@ governCmd
|
|
|
1400
1386
|
dir: options.dir,
|
|
1401
1387
|
config: options.config,
|
|
1402
1388
|
format: options.format,
|
|
1389
|
+
report: options.report,
|
|
1403
1390
|
quiet: options.quiet,
|
|
1391
|
+
apiKey: options.apiKey ?? process.env.FRAGMENTS_API_KEY,
|
|
1392
|
+
cloudUrl: options.cloudUrl ?? process.env.FRAGMENTS_CLOUD_URL,
|
|
1393
|
+
diff: options.diff,
|
|
1404
1394
|
});
|
|
1405
1395
|
process.exit(exitCode);
|
|
1406
1396
|
} catch (error) {
|
|
@@ -1431,28 +1421,5 @@ governCmd
|
|
|
1431
1421
|
}
|
|
1432
1422
|
});
|
|
1433
1423
|
|
|
1434
|
-
governCmd
|
|
1435
|
-
.command('push-contracts')
|
|
1436
|
-
.description('Push component contracts to Fragments Cloud')
|
|
1437
|
-
.option('-i, --input <path>', 'Path to fragments.json (default: ./fragments.json)')
|
|
1438
|
-
.option('--url <url>', 'Fragments Cloud URL')
|
|
1439
|
-
.option('--api-key <key>', 'API key (default: FRAGMENTS_API_KEY env var)')
|
|
1440
|
-
.option('-q, --quiet', 'Suppress non-error output')
|
|
1441
|
-
.action(async (options) => {
|
|
1442
|
-
try {
|
|
1443
|
-
const { pushContracts } = await import('./commands/push-contracts.js');
|
|
1444
|
-
const { exitCode } = await pushContracts({
|
|
1445
|
-
input: options.input,
|
|
1446
|
-
url: options.url,
|
|
1447
|
-
apiKey: options.apiKey,
|
|
1448
|
-
quiet: options.quiet,
|
|
1449
|
-
});
|
|
1450
|
-
process.exit(exitCode);
|
|
1451
|
-
} catch (error) {
|
|
1452
|
-
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1453
|
-
process.exit(1);
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
1424
|
// Parse command line arguments
|
|
1458
1425
|
program.parse();
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { strToU8, zipSync } from 'fflate';
|
|
6
|
+
|
|
7
|
+
const confirmMock = vi.hoisted(() => vi.fn());
|
|
8
|
+
|
|
9
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
10
|
+
confirm: confirmMock,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
function makeManifest(
|
|
14
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
15
|
+
) {
|
|
16
|
+
return {
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
catalogRevision: 'abc123',
|
|
19
|
+
catalogUpdatedAt: '2026-04-01T12:00:00.000Z',
|
|
20
|
+
org: {
|
|
21
|
+
id: 'org_123',
|
|
22
|
+
name: 'Acme',
|
|
23
|
+
slug: 'acme',
|
|
24
|
+
},
|
|
25
|
+
designSystem: {
|
|
26
|
+
name: 'Acme UI',
|
|
27
|
+
packageName: '@acme/react',
|
|
28
|
+
importPath: '@acme/react',
|
|
29
|
+
},
|
|
30
|
+
sourceBinding: {
|
|
31
|
+
bindingId: 'binding_123',
|
|
32
|
+
projectId: 'project_123',
|
|
33
|
+
projectName: 'Acme UI',
|
|
34
|
+
repoFullName: 'acme/ui',
|
|
35
|
+
resolution: 'explicit',
|
|
36
|
+
},
|
|
37
|
+
totalComponents: 2,
|
|
38
|
+
totalTokens: 1,
|
|
39
|
+
tokenCategories: {
|
|
40
|
+
color: 1,
|
|
41
|
+
},
|
|
42
|
+
components: {
|
|
43
|
+
component_1: {
|
|
44
|
+
componentId: 'component_1',
|
|
45
|
+
file: '.fragments/components/button-a7f3e2b1.json',
|
|
46
|
+
name: 'Button',
|
|
47
|
+
publicSlug: 'button',
|
|
48
|
+
tier: 'core',
|
|
49
|
+
category: 'inputs',
|
|
50
|
+
status: 'stable',
|
|
51
|
+
description: 'Primary action trigger.',
|
|
52
|
+
propCount: 2,
|
|
53
|
+
hasExamples: true,
|
|
54
|
+
hasCompoundChildren: false,
|
|
55
|
+
relations: [],
|
|
56
|
+
compoundChildren: [],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeBundleZip(manifestOverrides: Partial<Record<string, unknown>> = {}) {
|
|
64
|
+
const manifest = makeManifest(manifestOverrides);
|
|
65
|
+
const files = {
|
|
66
|
+
'.fragments/manifest.json': JSON.stringify(manifest, null, 2),
|
|
67
|
+
'.fragments/tokens.json': JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
catalogRevision: manifest.catalogRevision,
|
|
71
|
+
catalogUpdatedAt: manifest.catalogUpdatedAt,
|
|
72
|
+
categories: { color: { count: 1, tokens: [] } },
|
|
73
|
+
flat: [],
|
|
74
|
+
},
|
|
75
|
+
null,
|
|
76
|
+
2,
|
|
77
|
+
),
|
|
78
|
+
'.fragments/design-system.md': '# Design System: Acme UI',
|
|
79
|
+
'.fragments/README.md': '# Acme UI Fragments Bundle',
|
|
80
|
+
'.fragments/instructions/agents.md': 'Agents helper',
|
|
81
|
+
'.fragments/instructions/claude-code.md': 'Claude helper',
|
|
82
|
+
'.fragments/instructions/copilot.md': 'Copilot helper',
|
|
83
|
+
'.cursor/rules/fragments-design-system.mdc': 'Cursor helper',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return zipSync(
|
|
87
|
+
Object.fromEntries(
|
|
88
|
+
Object.entries(files).map(([path, content]) => [path, strToU8(content)]),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('context-cloud commands', () => {
|
|
94
|
+
let cwd: string;
|
|
95
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
96
|
+
|
|
97
|
+
beforeEach(async () => {
|
|
98
|
+
cwd = await mkdtemp(join(tmpdir(), 'fragments-context-cloud-'));
|
|
99
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
100
|
+
confirmMock.mockReset();
|
|
101
|
+
vi.stubGlobal('fetch', vi.fn(async (input: string | URL) => {
|
|
102
|
+
const url = new URL(String(input));
|
|
103
|
+
if (url.pathname === '/api/bundle') {
|
|
104
|
+
return new Response(makeBundleZip(), {
|
|
105
|
+
status: 200,
|
|
106
|
+
headers: { 'Content-Type': 'application/zip' },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (url.pathname === '/api/bundle-artifact') {
|
|
110
|
+
return Response.json({
|
|
111
|
+
content: JSON.stringify(makeManifest(), null, 2),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
consoleLogSpy.mockRestore();
|
|
120
|
+
vi.unstubAllGlobals();
|
|
121
|
+
delete process.env.FRAGMENTS_API_KEY;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('fails fast when no API key is provided', async () => {
|
|
125
|
+
const { contextInstallCloud } = await import('../context-cloud');
|
|
126
|
+
|
|
127
|
+
await expect(
|
|
128
|
+
contextInstallCloud({ cwd, rootFiles: 'never' }),
|
|
129
|
+
).rejects.toThrow(
|
|
130
|
+
'Missing Fragments Cloud API key. Set FRAGMENTS_API_KEY or pass --api-key.',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('installs the downloaded bundle into an empty repo', async () => {
|
|
135
|
+
const { contextInstallCloud } = await import('../context-cloud');
|
|
136
|
+
|
|
137
|
+
await contextInstallCloud({
|
|
138
|
+
cwd,
|
|
139
|
+
apiKey: 'fc_test_key',
|
|
140
|
+
rootFiles: 'never',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await expect(
|
|
144
|
+
readFile(join(cwd, '.fragments/manifest.json'), 'utf-8'),
|
|
145
|
+
).resolves.toContain('"schemaVersion": 1');
|
|
146
|
+
await expect(
|
|
147
|
+
readFile(
|
|
148
|
+
join(cwd, '.fragments/instructions/agents.md'),
|
|
149
|
+
'utf-8',
|
|
150
|
+
),
|
|
151
|
+
).resolves.toContain('Agents helper');
|
|
152
|
+
await expect(
|
|
153
|
+
readFile(
|
|
154
|
+
join(cwd, '.cursor/rules/fragments-design-system.mdc'),
|
|
155
|
+
'utf-8',
|
|
156
|
+
),
|
|
157
|
+
).resolves.toContain('Cursor helper');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('patches root files in prompt mode after confirmation', async () => {
|
|
161
|
+
const { contextInstallCloud } = await import('../context-cloud');
|
|
162
|
+
confirmMock.mockResolvedValue(true);
|
|
163
|
+
await writeFile(join(cwd, 'AGENTS.md'), 'Existing guidance', 'utf-8');
|
|
164
|
+
|
|
165
|
+
await contextInstallCloud({
|
|
166
|
+
cwd,
|
|
167
|
+
apiKey: 'fc_test_key',
|
|
168
|
+
rootFiles: 'prompt',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await expect(readFile(join(cwd, 'AGENTS.md'), 'utf-8')).resolves.toContain(
|
|
172
|
+
'BEGIN FRAGMENTS DESIGN SYSTEM',
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('does not patch root files in non-interactive mode without explicit patch', async () => {
|
|
177
|
+
const { contextInstallCloud } = await import('../context-cloud');
|
|
178
|
+
await writeFile(join(cwd, 'AGENTS.md'), 'Existing guidance', 'utf-8');
|
|
179
|
+
|
|
180
|
+
await contextInstallCloud({
|
|
181
|
+
cwd,
|
|
182
|
+
apiKey: 'fc_test_key',
|
|
183
|
+
yes: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await expect(readFile(join(cwd, 'AGENTS.md'), 'utf-8')).resolves.toBe(
|
|
187
|
+
'Existing guidance',
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('can opt .fragments out of git by updating .gitignore', async () => {
|
|
192
|
+
const { contextInstallCloud } = await import('../context-cloud');
|
|
193
|
+
await writeFile(join(cwd, '.gitignore'), 'node_modules/\n', 'utf-8');
|
|
194
|
+
|
|
195
|
+
await contextInstallCloud({
|
|
196
|
+
cwd,
|
|
197
|
+
apiKey: 'fc_test_key',
|
|
198
|
+
rootFiles: 'never',
|
|
199
|
+
gitignoreFragments: true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await expect(readFile(join(cwd, '.gitignore'), 'utf-8')).resolves.toContain(
|
|
203
|
+
'.fragments/',
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('surfaces a readable error when the bundle download is corrupted', async () => {
|
|
208
|
+
const { contextInstallCloud } = await import('../context-cloud');
|
|
209
|
+
vi.stubGlobal('fetch', vi.fn(async (input: string | URL) => {
|
|
210
|
+
const url = new URL(String(input));
|
|
211
|
+
if (url.pathname === '/api/bundle') {
|
|
212
|
+
return new Response('not-a-zip', {
|
|
213
|
+
status: 200,
|
|
214
|
+
headers: { 'Content-Type': 'application/zip' },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
await expect(
|
|
221
|
+
contextInstallCloud({
|
|
222
|
+
cwd,
|
|
223
|
+
apiKey: 'fc_test_key',
|
|
224
|
+
rootFiles: 'never',
|
|
225
|
+
}),
|
|
226
|
+
).rejects.toThrow('Bundle download was corrupted, try again.');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('reports status by comparing local and remote catalog revisions', async () => {
|
|
230
|
+
const { contextStatusCloud } = await import('../context-cloud');
|
|
231
|
+
await mkdir(join(cwd, '.fragments'), { recursive: true });
|
|
232
|
+
await writeFile(
|
|
233
|
+
join(cwd, '.fragments/manifest.json'),
|
|
234
|
+
JSON.stringify(makeManifest({ catalogRevision: 'old-revision' }), null, 2),
|
|
235
|
+
'utf-8',
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await contextStatusCloud({
|
|
239
|
+
cwd,
|
|
240
|
+
apiKey: 'fc_test_key',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Status: outdated'));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('rejects unsupported bundle schema versions with a clear upgrade message', async () => {
|
|
247
|
+
const { contextStatusCloud } = await import('../context-cloud');
|
|
248
|
+
await mkdir(join(cwd, '.fragments'), { recursive: true });
|
|
249
|
+
await writeFile(
|
|
250
|
+
join(cwd, '.fragments/manifest.json'),
|
|
251
|
+
JSON.stringify(makeManifest({ schemaVersion: 2 }), null, 2),
|
|
252
|
+
'utf-8',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await expect(
|
|
256
|
+
contextStatusCloud({
|
|
257
|
+
cwd,
|
|
258
|
+
apiKey: 'fc_test_key',
|
|
259
|
+
}),
|
|
260
|
+
).rejects.toThrow('Unsupported Fragments bundle schemaVersion 2. Upgrade your CLI.');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('rejects unsupported remote schema versions with the same upgrade guidance', async () => {
|
|
264
|
+
const { contextStatusCloud } = await import('../context-cloud');
|
|
265
|
+
await mkdir(join(cwd, '.fragments'), { recursive: true });
|
|
266
|
+
await writeFile(
|
|
267
|
+
join(cwd, '.fragments/manifest.json'),
|
|
268
|
+
JSON.stringify(makeManifest(), null, 2),
|
|
269
|
+
'utf-8',
|
|
270
|
+
);
|
|
271
|
+
vi.stubGlobal('fetch', vi.fn(async (input: string | URL) => {
|
|
272
|
+
const url = new URL(String(input));
|
|
273
|
+
if (url.pathname === '/api/bundle-artifact') {
|
|
274
|
+
return Response.json({
|
|
275
|
+
content: JSON.stringify(makeManifest({ schemaVersion: 2 }), null, 2),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return new Response(makeBundleZip(), {
|
|
279
|
+
status: 200,
|
|
280
|
+
headers: { 'Content-Type': 'application/zip' },
|
|
281
|
+
});
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
await expect(
|
|
285
|
+
contextStatusCloud({
|
|
286
|
+
cwd,
|
|
287
|
+
apiKey: 'fc_test_key',
|
|
288
|
+
}),
|
|
289
|
+
).rejects.toThrow('Unsupported Fragments bundle schemaVersion 2. Upgrade your CLI.');
|
|
290
|
+
});
|
|
291
|
+
});
|