@contentful/experience-design-system-cli 2.24.1-dev-build-f3943ba.0 → 2.24.1-dev-build-5ff9f2f.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 (47) hide show
  1. package/README.md +26 -0
  2. package/dist/package.json +2 -1
  3. package/dist/src/analytics/apply.d.ts +6 -0
  4. package/dist/src/analytics/apply.js +30 -0
  5. package/dist/src/analytics/client.d.ts +8 -0
  6. package/dist/src/analytics/client.js +76 -0
  7. package/dist/src/analytics/constants.d.ts +4 -0
  8. package/dist/src/analytics/constants.js +4 -0
  9. package/dist/src/analytics/env.d.ts +4 -0
  10. package/dist/src/analytics/env.js +14 -0
  11. package/dist/src/analytics/exit.d.ts +5 -0
  12. package/dist/src/analytics/exit.js +24 -0
  13. package/dist/src/analytics/index.d.ts +10 -0
  14. package/dist/src/analytics/index.js +9 -0
  15. package/dist/src/analytics/normalize.d.ts +3 -0
  16. package/dist/src/analytics/normalize.js +18 -0
  17. package/dist/src/analytics/os.d.ts +2 -0
  18. package/dist/src/analytics/os.js +13 -0
  19. package/dist/src/analytics/session.d.ts +3 -0
  20. package/dist/src/analytics/session.js +15 -0
  21. package/dist/src/analytics/tracker.d.ts +17 -0
  22. package/dist/src/analytics/tracker.js +126 -0
  23. package/dist/src/analytics/types.d.ts +28 -0
  24. package/dist/src/analytics/types.js +1 -0
  25. package/dist/src/analyze/command.js +8 -2
  26. package/dist/src/analyze/select/command.js +13 -9
  27. package/dist/src/analyze/select-agent/command.js +14 -11
  28. package/dist/src/apply/api-client.d.ts +3 -0
  29. package/dist/src/apply/api-client.js +14 -1
  30. package/dist/src/apply/command.d.ts +1 -1
  31. package/dist/src/apply/command.js +72 -50
  32. package/dist/src/apply/tui/ServerApplyView.js +1 -1
  33. package/dist/src/credentials-store.d.ts +2 -0
  34. package/dist/src/credentials-store.js +3 -1
  35. package/dist/src/generate/command.js +15 -9
  36. package/dist/src/generate/edit/command.js +1 -0
  37. package/dist/src/import/orchestrator.js +25 -12
  38. package/dist/src/import/tui/steps/DoneStep.js +1 -1
  39. package/dist/src/index.js +6 -1
  40. package/dist/src/lib/contentful-urls.d.ts +3 -15
  41. package/dist/src/lib/contentful-urls.js +2 -16
  42. package/dist/src/print/command.js +16 -13
  43. package/dist/src/program.js +11 -1
  44. package/dist/src/setup/analytics-prompt.d.ts +13 -0
  45. package/dist/src/setup/analytics-prompt.js +24 -0
  46. package/dist/src/setup/command.js +16 -0
  47. package/package.json +6 -5
@@ -11,6 +11,7 @@ import { buildRepoContextIndex, buildSelectionContext } from './context-builder.
11
11
  import { runShowRationale } from './show-rationale.js';
12
12
  import { isAbsolute, resolve } from 'node:path';
13
13
  import { getDebugLogger } from '../../lib/debug-logger.js';
14
+ import { bindAnalyticsSessionId, enrichCommandResult, exitWithAnalytics } from '../../analytics/index.js';
14
15
  import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
15
16
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
16
17
  export const DEFAULT_CONCURRENCY = 10;
@@ -27,7 +28,7 @@ function resolveBatchSize() {
27
28
  return DEFAULT_BATCH_SIZE;
28
29
  return Math.floor(n);
29
30
  }
30
- function resolveSessionId(sessionFlag) {
31
+ async function resolveSessionId(sessionFlag) {
31
32
  if (sessionFlag)
32
33
  return sessionFlag;
33
34
  const db = openPipelineDb();
@@ -42,7 +43,7 @@ function resolveSessionId(sessionFlag) {
42
43
  .get();
43
44
  if (!row) {
44
45
  process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
45
- process.exit(1);
46
+ return await exitWithAnalytics(1);
46
47
  }
47
48
  return row.id;
48
49
  }
@@ -329,7 +330,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
329
330
  catch (err) {
330
331
  const message = err instanceof Error ? err.message : String(err);
331
332
  process.stderr.write(`Error: ${message}\n`);
332
- process.exit(1);
333
+ await exitWithAnalytics(1);
333
334
  return;
334
335
  }
335
336
  }
@@ -338,7 +339,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
338
339
  const model = opts.model ?? savedCreds.agentModel;
339
340
  if (!agentName || !isAgentName(agentName)) {
340
341
  process.stderr.write(`Error: no agent configured. Pass --agent <name> or run experiences setup. Accepted values: ${AGENT_NAMES.join(', ')}\n`);
341
- process.exit(1);
342
+ await exitWithAnalytics(1);
342
343
  return;
343
344
  }
344
345
  const agent = agentName;
@@ -352,7 +353,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
352
353
  .catch(() => false);
353
354
  if (!exists) {
354
355
  process.stderr.write(`Error: custom prompt path not found: ${resolvedPath}\n`);
355
- process.exit(1);
356
+ await exitWithAnalytics(1);
356
357
  return;
357
358
  }
358
359
  if (!selectPromptPath.toLowerCase().endsWith('.md')) {
@@ -360,7 +361,8 @@ export function registerAnalyzeSelectAgentCommand(program) {
360
361
  }
361
362
  process.stderr.write(formatCustomPromptBanner('select', resolvedPath));
362
363
  }
363
- const sessionId = resolveSessionId(opts.session);
364
+ const sessionId = await resolveSessionId(opts.session);
365
+ await bindAnalyticsSessionId(sessionId);
364
366
  const selectionRoot = resolveProjectRoot(sessionId, opts.projectRoot);
365
367
  const db = openPipelineDb();
366
368
  let rawComponents;
@@ -374,7 +376,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
374
376
  }
375
377
  if (rawComponents.length === 0) {
376
378
  process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
377
- process.exit(1);
379
+ await exitWithAnalytics(1);
378
380
  return;
379
381
  }
380
382
  // Re-run validation (not persisted to DB, so always recompute).
@@ -399,7 +401,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
399
401
  lines.push('');
400
402
  lines.push('Re-run with --exclude-invalid to auto-reject these components, or fix them in source first.');
401
403
  process.stderr.write(lines.join('\n') + '\n');
402
- process.exit(1);
404
+ await exitWithAnalytics(1);
403
405
  return;
404
406
  }
405
407
  const componentsForAgent = validatedComponents.filter((comp) => !shouldExcludeDueToValidation(comp));
@@ -426,7 +428,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
426
428
  if (opts.dryRun) {
427
429
  if (selectionCandidates.length === 0) {
428
430
  process.stderr.write('No valid components to preview — all components were excluded due to validation errors.\n');
429
- process.exit(0);
431
+ await exitWithAnalytics(0);
430
432
  return;
431
433
  }
432
434
  const first = selectionCandidates[0];
@@ -438,7 +440,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
438
440
  skillPathOverride: selectPromptPath ? resolve(selectPromptPath) : undefined,
439
441
  });
440
442
  process.stdout.write(prompt + '\n');
441
- process.exit(0);
443
+ await exitWithAnalytics(0);
442
444
  return;
443
445
  }
444
446
  // --no-cache is the global kill-switch; --no-select-cache is the stage-
@@ -504,7 +506,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
504
506
  }
505
507
  catch (error) {
506
508
  process.stderr.write(`Error: unable to initialize select session.\n${error instanceof Error ? error.message : String(error)}\n`);
507
- process.exit(1);
509
+ await exitWithAnalytics(1);
508
510
  return;
509
511
  }
510
512
  const paths = await getRefineSessionPaths(sessionId, artifactsRoot);
@@ -550,6 +552,7 @@ export function registerAnalyzeSelectAgentCommand(program) {
550
552
  finally {
551
553
  stepDb.close();
552
554
  }
555
+ enrichCommandResult({ accepted_component_count: accepted.length });
553
556
  process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length} Needs review: ${unresolved.length}\n`);
554
557
  });
555
558
  }
@@ -45,6 +45,9 @@ export declare class ImportApiClient {
45
45
  private spaceId;
46
46
  private environmentId;
47
47
  private retry;
48
+ private lastRequestId?;
49
+ getLastRequestId(): string | undefined;
50
+ private noteRequestId;
48
51
  constructor(opts: ApiClientOptions);
49
52
  private base;
50
53
  private headers;
@@ -180,6 +180,15 @@ export class ImportApiClient {
180
180
  spaceId;
181
181
  environmentId;
182
182
  retry;
183
+ lastRequestId;
184
+ getLastRequestId() {
185
+ return this.lastRequestId;
186
+ }
187
+ noteRequestId(response) {
188
+ const requestId = response.headers.get('x-contentful-request-id');
189
+ if (requestId)
190
+ this.lastRequestId = requestId;
191
+ }
183
192
  constructor(opts) {
184
193
  this.host = toApiHost(opts.host);
185
194
  this.token = opts.cmaToken;
@@ -237,8 +246,10 @@ export class ImportApiClient {
237
246
  await this.retry.sleep(delayMs);
238
247
  continue;
239
248
  }
240
- if (!isTransientStatus(result.response.status))
249
+ if (!isTransientStatus(result.response.status)) {
250
+ this.noteRequestId(result.response);
241
251
  return result;
252
+ }
242
253
  if (attempt === this.retry.maxAttempts) {
243
254
  const body = stringifyError(result.error);
244
255
  const guidance = `The ${phase} request failed after ${attempt} attempts because the service remained unavailable. ` +
@@ -273,6 +284,7 @@ export class ImportApiClient {
273
284
  // false positives that don't apply to the design-systems API authorization path.
274
285
  const url = `${this.host}/users/me`;
275
286
  const res = await request(url, { token: this.token });
287
+ this.noteRequestId(res);
276
288
  if (res.status === 401) {
277
289
  throw new ApiError('CMA token is invalid or revoked', res.status, await res.text());
278
290
  }
@@ -298,6 +310,7 @@ export class ImportApiClient {
298
310
  catch {
299
311
  return; // network error — don't block; the real call will surface it
300
312
  }
313
+ this.noteRequestId(res);
301
314
  if (res.ok)
302
315
  return;
303
316
  if (res.status >= 500)
@@ -11,7 +11,7 @@ export declare function formatSlotCycleReport(cycles: ReturnType<typeof findSlot
11
11
  export declare function assertNoSlotCycles(components: Array<{
12
12
  key: string;
13
13
  entry: CDFComponentEntry;
14
- }>): void;
14
+ }>): Promise<void>;
15
15
  export declare function extractComponentsFromManifest(manifest: {
16
16
  componentsManifest?: Record<string, unknown>;
17
17
  } | null | undefined): Array<{
@@ -16,9 +16,10 @@ import { addAllowDeletionsOption, addArtifactInputOptions, addCompositionOptions
16
16
  import { stripAllowedComponents } from '../import/strip-allowed-components.js';
17
17
  import { readExperiencesCredentials } from '../credentials-store.js';
18
18
  import { getInteractiveTerminalSupport, requireInteractiveTerminal } from '../lib/terminal-capabilities.js';
19
- function die(message) {
19
+ import { bindAnalyticsSessionId, exitWithAnalytics, failureFromApiError, recordApplyOutcome, recordContentfulContext, } from '../analytics/index.js';
20
+ async function die(message, fields = {}) {
20
21
  process.stderr.write(`${message}\n`);
21
- process.exit(1);
22
+ return exitWithAnalytics(1, fields);
22
23
  }
23
24
  async function pathExists(p) {
24
25
  return access(p)
@@ -27,7 +28,7 @@ async function pathExists(p) {
27
28
  }
28
29
  async function assertFileExists(flag, p) {
29
30
  if (!(await pathExists(p)))
30
- die(`Error: file not found: ${p} (from ${flag})`);
31
+ return await die(`Error: file not found: ${p} (from ${flag})`);
31
32
  }
32
33
  async function readJsonFile(flag, p) {
33
34
  let text;
@@ -35,13 +36,13 @@ async function readJsonFile(flag, p) {
35
36
  text = await readFile(p, 'utf8');
36
37
  }
37
38
  catch {
38
- die(`Error: file not found: ${p} (from ${flag})`);
39
+ return await die(`Error: file not found: ${p} (from ${flag})`);
39
40
  }
40
41
  try {
41
42
  return JSON.parse(text);
42
43
  }
43
44
  catch {
44
- die(`Error: ${flag} is not valid JSON: ${p}`);
45
+ return await die(`Error: ${flag} is not valid JSON: ${p}`);
45
46
  }
46
47
  }
47
48
  const IGNORE_TOKEN_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.nuxt', '.git']);
@@ -83,12 +84,12 @@ export async function readTokensFromPath(flag, p) {
83
84
  s = await stat(p);
84
85
  }
85
86
  catch {
86
- die(`Error: file not found: ${p} (from ${flag})`);
87
+ return await die(`Error: file not found: ${p} (from ${flag})`);
87
88
  }
88
89
  if (s.isDirectory()) {
89
90
  const files = await collectJsonFiles(p);
90
91
  if (files.length === 0)
91
- die(`Error: no .json files found in directory: ${p} (from ${flag})`);
92
+ return await die(`Error: no .json files found in directory: ${p} (from ${flag})`);
92
93
  const merged = {};
93
94
  for (const file of files.sort()) {
94
95
  let text;
@@ -111,36 +112,36 @@ export async function readTokensFromPath(flag, p) {
111
112
  }
112
113
  const { valid, errors } = validateDTCG(merged);
113
114
  if (!valid)
114
- die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
115
+ return await die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
115
116
  return flattenDTCG(merged, '');
116
117
  }
117
118
  const raw = await readJsonFile(flag, p);
118
119
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
119
- die(`Error: ${flag} is not valid JSON: expected an object`);
120
+ return await die(`Error: ${flag} is not valid JSON: expected an object`);
120
121
  }
121
122
  const { valid, errors } = validateDTCG(raw);
122
123
  if (!valid)
123
- die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
124
+ return await die(`Error: ${flag} contains invalid token types:\n${errors.map((e) => ` ${e.path}: ${e.message}`).join('\n')}`);
124
125
  return flattenDTCG(raw, '');
125
126
  }
126
127
  async function resolveSharedInputs(opts) {
127
128
  if (!opts.components && !opts.tokens && !opts.session) {
128
- die('Error: at least one of --components, --tokens, or --session is required');
129
+ return await die('Error: at least one of --components, --tokens, or --session is required');
129
130
  }
130
131
  if (opts.session && opts.components) {
131
- die('Error: --session and --components are mutually exclusive');
132
+ return await die('Error: --session and --components are mutually exclusive');
132
133
  }
133
134
  const spaceId = opts.spaceId ?? process.env.CONTENTFUL_SPACE_ID;
134
135
  const environmentId = opts.environmentId ?? process.env.CONTENTFUL_ENVIRONMENT_ID;
135
136
  if (!spaceId)
136
- die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
137
+ return await die('Error: --space-id is required (or set CONTENTFUL_SPACE_ID)');
137
138
  if (!environmentId)
138
- die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
139
+ return await die('Error: --environment-id is required (or set CONTENTFUL_ENVIRONMENT_ID)');
139
140
  opts.spaceId = spaceId;
140
141
  opts.environmentId = environmentId;
141
142
  const cmaToken = opts.cmaToken ?? process.env.CONTENTFUL_MANAGEMENT_TOKEN;
142
143
  if (!cmaToken) {
143
- die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
144
+ return await die('Error: CMA token is required. Pass --cma-token or set CONTENTFUL_MANAGEMENT_TOKEN');
144
145
  }
145
146
  if (opts.components)
146
147
  await assertFileExists('--components', opts.components);
@@ -154,14 +155,14 @@ async function resolveSharedInputs(opts) {
154
155
  db.close();
155
156
  }
156
157
  if (components.length === 0) {
157
- die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
158
+ return await die(`Error: session '${opts.session}' has no generated components. Run generate components first.`);
158
159
  }
159
160
  }
160
161
  else if (opts.components) {
161
162
  const raw = await readJsonFile('--components', opts.components);
162
163
  const result = validateCDF(raw);
163
164
  if (!result.valid) {
164
- die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
165
+ return await die(`Error: --components failed schema validation: ${result.errors.map((e) => e.message).join(', ')}`);
165
166
  }
166
167
  components = result.components;
167
168
  }
@@ -213,12 +214,12 @@ export function formatSlotCycleReport(cycles) {
213
214
  }
214
215
  return lines;
215
216
  }
216
- export function assertNoSlotCycles(components) {
217
+ export async function assertNoSlotCycles(components) {
217
218
  const cycles = detectSlotCycles(components);
218
219
  if (cycles.length === 0)
219
220
  return;
220
221
  process.stderr.write(formatSlotCycleReport(cycles).join('\n') + '\n');
221
- process.exit(1);
222
+ await exitWithAnalytics(1);
222
223
  }
223
224
  export function extractComponentsFromManifest(manifest) {
224
225
  const componentsManifest = manifest?.componentsManifest;
@@ -287,6 +288,7 @@ function buildApplyOutput(operation, spaceId, environmentId, host) {
287
288
  componentTypes: countByAction(componentItems),
288
289
  designTokens: countByAction(tokenItems),
289
290
  viewUrl: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId }),
291
+ tokensUrl: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId, view: 'design_tokens' }),
290
292
  failures: items
291
293
  .filter((item) => item.status === 'failed')
292
294
  .map((item) => ({
@@ -427,18 +429,24 @@ export function registerApplyCommand(program) {
427
429
  }
428
430
  catch (e) {
429
431
  if (e instanceof ApiError)
430
- die(`Error: ${formatApiError(e)}`);
432
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
431
433
  throw e;
432
434
  }
433
435
  const { components, tokens, client } = inputs;
436
+ const spaceId = opts.spaceId;
437
+ const environmentId = opts.environmentId;
438
+ await bindAnalyticsSessionId(opts.session, {
439
+ space_key: spaceId,
440
+ environment_key: environmentId,
441
+ });
434
442
  try {
435
443
  await client.validateToken();
436
444
  }
437
445
  catch (e) {
438
446
  if (e instanceof ApiError)
439
- die(`Error: ${formatApiError(e)}`);
447
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
440
448
  const cause = e instanceof Error && e.cause instanceof Error ? e.cause.message : '';
441
- die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
449
+ return await die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
442
450
  }
443
451
  const manifest = buildManifest(components, tokens);
444
452
  let preview;
@@ -447,11 +455,10 @@ export function registerApplyCommand(program) {
447
455
  }
448
456
  catch (e) {
449
457
  if (e instanceof ApiError)
450
- die(`Error: ${formatApiError(e)}`);
458
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
451
459
  throw e;
452
460
  }
453
- const spaceId = opts.spaceId;
454
- const environmentId = opts.environmentId;
461
+ recordContentfulContext(client, spaceId, environmentId);
455
462
  if (getInteractiveTerminalSupport().supported) {
456
463
  const { waitUntilExit } = render(createElement(ServerPreviewApp, {
457
464
  preview,
@@ -463,7 +470,7 @@ export function registerApplyCommand(program) {
463
470
  }
464
471
  else {
465
472
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
466
- process.exit(0);
473
+ await exitWithAnalytics(0);
467
474
  }
468
475
  });
469
476
  const pushCmd = applyCmd.command('push').description('Write component types and design tokens to Contentful ExO');
@@ -480,7 +487,7 @@ export function registerApplyCommand(program) {
480
487
  const isTTY = getInteractiveTerminalSupport().supported;
481
488
  if (!isTTY && !opts.yes) {
482
489
  process.stderr.write('Error: apply push requires --yes in non-interactive mode\n');
483
- process.exit(1);
490
+ await exitWithAnalytics(1);
484
491
  }
485
492
  let inputs;
486
493
  try {
@@ -488,17 +495,23 @@ export function registerApplyCommand(program) {
488
495
  }
489
496
  catch (e) {
490
497
  if (e instanceof ApiError)
491
- die(`Error: ${formatApiError(e, opts.verbose)}`);
498
+ return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
492
499
  throw e;
493
500
  }
494
501
  const { components, tokens, client } = inputs;
495
- assertNoSlotCycles(components);
502
+ const spaceId = opts.spaceId;
503
+ const environmentId = opts.environmentId;
504
+ await bindAnalyticsSessionId(opts.session, {
505
+ space_key: spaceId,
506
+ environment_key: environmentId,
507
+ });
508
+ await assertNoSlotCycles(components);
496
509
  try {
497
510
  await client.validateToken();
498
511
  }
499
512
  catch (e) {
500
513
  if (e instanceof ApiError)
501
- die(`Error: ${formatApiError(e, opts.verbose)}`);
514
+ return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
502
515
  throw e;
503
516
  }
504
517
  const manifest = buildManifest(components, tokens);
@@ -508,11 +521,10 @@ export function registerApplyCommand(program) {
508
521
  }
509
522
  catch (e) {
510
523
  if (e instanceof ApiError)
511
- die(`Error: ${formatApiError(e, opts.verbose)}`);
524
+ return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
512
525
  throw e;
513
526
  }
514
- const spaceId = opts.spaceId;
515
- const environmentId = opts.environmentId;
527
+ recordContentfulContext(client, spaceId, environmentId);
516
528
  if (opts.dryRun) {
517
529
  if (isTTY) {
518
530
  const { waitUntilExit } = render(createElement(ServerPreviewApp, {
@@ -526,7 +538,7 @@ export function registerApplyCommand(program) {
526
538
  else {
527
539
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
528
540
  }
529
- process.exit(0);
541
+ await exitWithAnalytics(0);
530
542
  }
531
543
  if (isEmptyPreview(preview)) {
532
544
  if (isTTY && !opts.yes) {
@@ -535,14 +547,14 @@ export function registerApplyCommand(program) {
535
547
  else {
536
548
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
537
549
  }
538
- process.exit(0);
550
+ await exitWithAnalytics(0);
539
551
  }
540
552
  const breakingWithImpact = hasBreakingChangesWithImpact(preview);
541
553
  if (!isTTY || opts.yes) {
542
554
  if (breakingWithImpact && !opts.force) {
543
555
  process.stderr.write('Error: breaking changes with downstream impact detected. Use --force to acknowledge.\n');
544
556
  process.stdout.write(JSON.stringify(buildPreviewOutput(preview, spaceId, environmentId), null, 2) + '\n');
545
- process.exit(1);
557
+ await exitWithAnalytics(1);
546
558
  }
547
559
  const verbose = opts.verbose ?? false;
548
560
  if (verbose) {
@@ -557,7 +569,7 @@ export function registerApplyCommand(program) {
557
569
  }
558
570
  catch (e) {
559
571
  if (e instanceof ApiError)
560
- die(`Error: ${formatApiError(e, opts.verbose)}`);
572
+ return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
561
573
  throw e;
562
574
  }
563
575
  process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
@@ -566,12 +578,14 @@ export function registerApplyCommand(program) {
566
578
  }
567
579
  catch (e) {
568
580
  if (e instanceof ApiError)
569
- die(`Error: ${formatApiError(e, opts.verbose)}`);
581
+ return await die(`Error: ${formatApiError(e, opts.verbose)}`, failureFromApiError(e));
570
582
  throw e;
571
583
  }
572
584
  const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
585
+ recordApplyOutcome(client, spaceId, environmentId, operation);
573
586
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
574
- process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
587
+ const exitCode = operation.sys.status === 'succeeded' ? 0 : 1;
588
+ await exitWithAnalytics(exitCode);
575
589
  return;
576
590
  }
577
591
  await new Promise((resolvePromise) => {
@@ -621,6 +635,7 @@ export function registerApplyCommand(program) {
621
635
  }
622
636
  throw e;
623
637
  }
638
+ recordApplyOutcome(client, spaceId, environmentId, operation);
624
639
  instance.rerender(createElement(ServerApplyDone, {
625
640
  operation,
626
641
  spaceId,
@@ -639,7 +654,7 @@ export function registerApplyCommand(program) {
639
654
  void runApply(acknowledge, applyDeletions);
640
655
  },
641
656
  onCancel: () => {
642
- process.exit(0);
657
+ void exitWithAnalytics(0);
643
658
  },
644
659
  }));
645
660
  void instance.waitUntilExit().then(() => resolvePromise());
@@ -664,17 +679,17 @@ export function registerApplyCommand(program) {
664
679
  }
665
680
  catch (e) {
666
681
  if (e instanceof ApiError)
667
- die(`Error: ${formatApiError(e)}`);
682
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
668
683
  throw e;
669
684
  }
670
685
  const { components, tokens, client } = inputs;
671
- assertNoSlotCycles(components);
686
+ await assertNoSlotCycles(components);
672
687
  try {
673
688
  await client.validateToken();
674
689
  }
675
690
  catch (e) {
676
691
  if (e instanceof ApiError)
677
- die(`Error: ${formatApiError(e)}`);
692
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
678
693
  throw e;
679
694
  }
680
695
  const fullManifest = buildManifest(components, tokens);
@@ -684,21 +699,26 @@ export function registerApplyCommand(program) {
684
699
  }
685
700
  catch (e) {
686
701
  if (e instanceof ApiError)
687
- die(`Error: ${formatApiError(e)}`);
702
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
688
703
  throw e;
689
704
  }
690
705
  const spaceId = opts.spaceId;
691
706
  const environmentId = opts.environmentId;
707
+ await bindAnalyticsSessionId(opts.session, {
708
+ space_key: spaceId,
709
+ environment_key: environmentId,
710
+ });
711
+ recordContentfulContext(client, spaceId, environmentId);
692
712
  const entities = getSelectableEntities(preview);
693
713
  if (entities.length === 0) {
694
714
  process.stderr.write('Nothing to change — design system is up to date.\n');
695
- process.exit(0);
715
+ await exitWithAnalytics(0);
696
716
  }
697
717
  if (nonInteractive) {
698
718
  const selectedKeys = resolveNonInteractiveSelection(entities, opts);
699
719
  if (selectedKeys.size === 0) {
700
720
  process.stderr.write('No entities matched selection criteria.\n');
701
- process.exit(0);
721
+ await exitWithAnalytics(0);
702
722
  }
703
723
  const selectedComponentKeys = new Set();
704
724
  const selectedTokenPaths = new Set();
@@ -714,7 +734,7 @@ export function registerApplyCommand(program) {
714
734
  const hasBreaking = entities.some((e) => e.isBreaking && selectedKeys.has(makeSelectKey(e.kind, e.id)));
715
735
  if (hasBreaking && !opts.force) {
716
736
  process.stderr.write('Error: selection includes breaking changes. Use --force to acknowledge.\n');
717
- process.exit(1);
737
+ await exitWithAnalytics(1);
718
738
  }
719
739
  let operation;
720
740
  try {
@@ -725,7 +745,7 @@ export function registerApplyCommand(program) {
725
745
  }
726
746
  catch (e) {
727
747
  if (e instanceof ApiError)
728
- die(`Error: ${formatApiError(e)}`);
748
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
729
749
  throw e;
730
750
  }
731
751
  process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
@@ -734,12 +754,13 @@ export function registerApplyCommand(program) {
734
754
  }
735
755
  catch (e) {
736
756
  if (e instanceof ApiError)
737
- die(`Error: ${formatApiError(e)}`);
757
+ return await die(`Error: ${formatApiError(e)}`, failureFromApiError(e));
738
758
  throw e;
739
759
  }
740
760
  const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
761
+ recordApplyOutcome(client, spaceId, environmentId, operation);
741
762
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
742
- process.exit(operation.sys.status === 'succeeded' ? 0 : 1);
763
+ await exitWithAnalytics(operation.sys.status === 'succeeded' ? 0 : 1);
743
764
  return;
744
765
  }
745
766
  await new Promise((resolvePromise) => {
@@ -801,6 +822,7 @@ export function registerApplyCommand(program) {
801
822
  }
802
823
  throw e;
803
824
  }
825
+ recordApplyOutcome(client, spaceId, environmentId, operation);
804
826
  instance.rerender(createElement(ServerApplyDone, {
805
827
  operation,
806
828
  spaceId,
@@ -42,7 +42,7 @@ export function ServerApplyDone({ operation, spaceId, environmentId, host }) {
42
42
  }
43
43
  });
44
44
  const failures = (operation.items ?? []).filter((item) => item.status === 'failed');
45
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), operation.sys.status === 'succeeded' && operation.summary.succeeded > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " View your design system:" }), _jsxs(Text, { color: "cyan", children: [" ", buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
45
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), operation.sys.status === 'succeeded' && operation.summary.succeeded > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " View your design system:" }), _jsxs(Text, { color: "cyan", children: [" ", buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId })] }), _jsx(Text, { dimColor: true, children: " View your design tokens:" }), _jsxs(Text, { color: "cyan", children: [' ', buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId, view: 'design_tokens' })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
46
46
  }
47
47
  function formatItemError(error) {
48
48
  return formatEdsiError(error);
@@ -13,6 +13,8 @@ export type ExperiencesCredentials = {
13
13
  autoFilter?: boolean;
14
14
  /** Feature: default debug-mode (writes JSONL trace of every decision) for all commands. */
15
15
  debug?: boolean;
16
+ /** Persisted opt-out for anonymous CLI usage analytics, set via `experiences setup`. */
17
+ analyticsDisabled?: boolean;
16
18
  /**
17
19
  * Feature (atomic mode): default composition mode. `atomic` (default) imports
18
20
  * flat components with no embedded-component hierarchy; `composite` opts into
@@ -36,6 +36,7 @@ export async function readExperiencesCredentials() {
36
36
  ...(parsed.generatePromptPath ? { generatePromptPath: parsed.generatePromptPath } : {}),
37
37
  ...(typeof parsed.autoFilter === 'boolean' ? { autoFilter: parsed.autoFilter } : {}),
38
38
  ...(typeof parsed.debug === 'boolean' ? { debug: parsed.debug } : {}),
39
+ ...(typeof parsed.analyticsDisabled === 'boolean' ? { analyticsDisabled: parsed.analyticsDisabled } : {}),
39
40
  ...(typeof parsed.compositionMode === 'string' && isCompositionMode(parsed.compositionMode)
40
41
  ? { compositionMode: parsed.compositionMode }
41
42
  : {}),
@@ -52,7 +53,7 @@ export async function readExperiencesCredentials() {
52
53
  }
53
54
  }
54
55
  export async function writeExperiencesCredentials(creds) {
55
- const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, compositionMode, ...rest } = creds;
56
+ const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, debug, analyticsDisabled, compositionMode, ...rest } = creds;
56
57
  const host = toConfiguredHost(creds.host);
57
58
  await mkdir(CREDENTIALS_DIR, { recursive: true });
58
59
  await writeFile(CREDENTIALS_PATH, JSON.stringify({
@@ -64,6 +65,7 @@ export async function writeExperiencesCredentials(creds) {
64
65
  ...(generatePromptPath ? { generatePromptPath } : {}),
65
66
  ...(typeof autoFilter === 'boolean' ? { autoFilter } : {}),
66
67
  ...(typeof debug === 'boolean' ? { debug } : {}),
68
+ ...(typeof analyticsDisabled === 'boolean' ? { analyticsDisabled } : {}),
67
69
  ...(compositionMode && isCompositionMode(compositionMode) ? { compositionMode } : {}),
68
70
  }, null, 2) + '\n', { mode: 0o600 });
69
71
  }