@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.
Files changed (88) hide show
  1. package/dist/bin.js +901 -789
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-6SQPP47U.js → chunk-ANTWP3UG.js} +532 -31
  4. package/dist/chunk-ANTWP3UG.js.map +1 -0
  5. package/dist/{chunk-ONUP6Z4W.js → chunk-B4A4ZEGS.js} +9 -9
  6. package/dist/{chunk-32LIWN2P.js → chunk-FFCI6OVZ.js} +584 -261
  7. package/dist/chunk-FFCI6OVZ.js.map +1 -0
  8. package/dist/{chunk-HQ6A6DTV.js → chunk-HNHE64CR.js} +315 -1089
  9. package/dist/chunk-HNHE64CR.js.map +1 -0
  10. package/dist/{chunk-BJE3425I.js → chunk-MN3B2EE6.js} +2 -2
  11. package/dist/{chunk-QCN35LJU.js → chunk-SAQW37L5.js} +3 -2
  12. package/dist/chunk-SAQW37L5.js.map +1 -0
  13. package/dist/{chunk-2WXKALIG.js → chunk-SNZXGHL2.js} +2 -2
  14. package/dist/{chunk-5JF26E55.js → chunk-VT2J62ND.js} +11 -11
  15. package/dist/{codebase-scanner-MQHUZC2G.js → codebase-scanner-2T5QIDBA.js} +2 -2
  16. package/dist/core/index.js +53 -1
  17. package/dist/{create-EXURTBKK.js → create-D44QD7MV.js} +2 -2
  18. package/dist/{doctor-BDPMYYE6.js → doctor-7B5N4JYU.js} +2 -2
  19. package/dist/{generate-PVOLUAAC.js → generate-T47JZRVU.js} +4 -4
  20. package/dist/govern-scan-X6UEIOSV.js +632 -0
  21. package/dist/govern-scan-X6UEIOSV.js.map +1 -0
  22. package/dist/index.js +7 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/{init-SSGUSP7Z.js → init-2RGAY4W6.js} +5 -5
  25. package/dist/mcp-bin.js +2 -2
  26. package/dist/scan-A2WJM54L.js +14 -0
  27. package/dist/{scan-generate-VY27PIOX.js → scan-generate-LUSOHT36.js} +4 -4
  28. package/dist/{service-QJGWUIVL.js → service-ROCP7TKG.js} +13 -15
  29. package/dist/{snapshot-WIJMEIFT.js → snapshot-B3SAW74Y.js} +2 -2
  30. package/dist/{static-viewer-7QIBQZRC.js → static-viewer-7L6UEYTJ.js} +3 -3
  31. package/dist/{test-64Z5BKBA.js → test-PQDVDURE.js} +3 -3
  32. package/dist/{token-normalizer-TEPOVBPV.js → token-normalizer-7TFCVDZL.js} +2 -2
  33. package/dist/{tokens-NZWFQIAB.js → tokens-64FG5FDP.js} +8 -9
  34. package/dist/{tokens-NZWFQIAB.js.map → tokens-64FG5FDP.js.map} +1 -1
  35. package/dist/{tokens-generate-5JQSJ27E.js → tokens-generate-CL4LBBQA.js} +2 -2
  36. package/package.json +9 -8
  37. package/src/bin.ts +55 -88
  38. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  39. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  40. package/src/commands/__tests__/context-cloud.test.ts +291 -0
  41. package/src/commands/__tests__/govern-scan.test.ts +185 -0
  42. package/src/commands/__tests__/govern.test.ts +1 -0
  43. package/src/commands/context-cloud.ts +355 -0
  44. package/src/commands/govern-scan-report.ts +170 -0
  45. package/src/commands/govern-scan.ts +282 -135
  46. package/src/commands/govern.ts +0 -157
  47. package/src/mcp/__tests__/server.integration.test.ts +9 -20
  48. package/src/service/enhance/codebase-scanner.ts +3 -2
  49. package/src/service/enhance/types.ts +3 -0
  50. package/dist/chunk-32LIWN2P.js.map +0 -1
  51. package/dist/chunk-6SQPP47U.js.map +0 -1
  52. package/dist/chunk-HQ6A6DTV.js.map +0 -1
  53. package/dist/chunk-MHIBEEW4.js +0 -511
  54. package/dist/chunk-MHIBEEW4.js.map +0 -1
  55. package/dist/chunk-QCN35LJU.js.map +0 -1
  56. package/dist/govern-scan-DW4QUAYD.js +0 -414
  57. package/dist/govern-scan-DW4QUAYD.js.map +0 -1
  58. package/dist/init-cloud-3DNKPWFB.js +0 -304
  59. package/dist/init-cloud-3DNKPWFB.js.map +0 -1
  60. package/dist/node-37AUE74M.js +0 -65
  61. package/dist/push-contracts-WY32TFP6.js +0 -84
  62. package/dist/push-contracts-WY32TFP6.js.map +0 -1
  63. package/dist/scan-PKSYSTRR.js +0 -15
  64. package/dist/static-viewer-7QIBQZRC.js.map +0 -1
  65. package/dist/token-parser-32KOIOFN.js +0 -22
  66. package/dist/token-parser-32KOIOFN.js.map +0 -1
  67. package/dist/tokens-push-HY3KO36V.js +0 -148
  68. package/dist/tokens-push-HY3KO36V.js.map +0 -1
  69. package/src/commands/init-cloud.ts +0 -382
  70. package/src/commands/push-contracts.ts +0 -112
  71. package/src/commands/tokens-push.ts +0 -199
  72. /package/dist/{chunk-ONUP6Z4W.js.map → chunk-B4A4ZEGS.js.map} +0 -0
  73. /package/dist/{chunk-BJE3425I.js.map → chunk-MN3B2EE6.js.map} +0 -0
  74. /package/dist/{chunk-2WXKALIG.js.map → chunk-SNZXGHL2.js.map} +0 -0
  75. /package/dist/{chunk-5JF26E55.js.map → chunk-VT2J62ND.js.map} +0 -0
  76. /package/dist/{codebase-scanner-MQHUZC2G.js.map → codebase-scanner-2T5QIDBA.js.map} +0 -0
  77. /package/dist/{create-EXURTBKK.js.map → create-D44QD7MV.js.map} +0 -0
  78. /package/dist/{doctor-BDPMYYE6.js.map → doctor-7B5N4JYU.js.map} +0 -0
  79. /package/dist/{generate-PVOLUAAC.js.map → generate-T47JZRVU.js.map} +0 -0
  80. /package/dist/{init-SSGUSP7Z.js.map → init-2RGAY4W6.js.map} +0 -0
  81. /package/dist/{node-37AUE74M.js.map → scan-A2WJM54L.js.map} +0 -0
  82. /package/dist/{scan-generate-VY27PIOX.js.map → scan-generate-LUSOHT36.js.map} +0 -0
  83. /package/dist/{scan-PKSYSTRR.js.map → service-ROCP7TKG.js.map} +0 -0
  84. /package/dist/{snapshot-WIJMEIFT.js.map → snapshot-B3SAW74Y.js.map} +0 -0
  85. /package/dist/{service-QJGWUIVL.js.map → static-viewer-7L6UEYTJ.js.map} +0 -0
  86. /package/dist/{test-64Z5BKBA.js.map → test-PQDVDURE.js.map} +0 -0
  87. /package/dist/{token-normalizer-TEPOVBPV.js.map → token-normalizer-7TFCVDZL.js.map} +0 -0
  88. /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, governConnect } from './commands/govern.js';
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 for your design system')
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
- .action(async (options) => {
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();
@@ -37,6 +37,6 @@
37
37
  "source": "extracted",
38
38
  "verified": false,
39
39
  "frameworkSupport": "native",
40
- "extractedAt": "2026-03-26T14:38:30.456Z"
40
+ "extractedAt": "2026-04-04T15:06:36.231Z"
41
41
  }
42
42
  }
@@ -15,6 +15,6 @@
15
15
  "source": "extracted",
16
16
  "verified": false,
17
17
  "frameworkSupport": "native",
18
- "extractedAt": "2026-03-26T14:38:30.457Z"
18
+ "extractedAt": "2026-04-04T15:06:36.234Z"
19
19
  }
20
20
  }
@@ -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
+ });