@contentful/experience-design-system-cli 2.23.2-dev-build-09d87bb.0 → 2.23.2-dev-build-753ceef.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 (69) hide show
  1. package/README.md +24 -2
  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 +7 -0
  6. package/dist/src/analytics/client.js +68 -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/tui/App.js +1 -4
  28. package/dist/src/analyze/select-agent/command.js +14 -11
  29. package/dist/src/apply/api-client.d.ts +5 -5
  30. package/dist/src/apply/api-client.js +18 -8
  31. package/dist/src/apply/command.d.ts +1 -1
  32. package/dist/src/apply/command.js +81 -77
  33. package/dist/src/apply/preview-utils.d.ts +1 -5
  34. package/dist/src/apply/preview-utils.js +1 -5
  35. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -6
  36. package/dist/src/apply/tui/ServerApplyView.js +6 -15
  37. package/dist/src/apply/tui/ServerPreviewView.d.ts +1 -2
  38. package/dist/src/apply/tui/ServerPreviewView.js +3 -3
  39. package/dist/src/generate/command.js +15 -9
  40. package/dist/src/generate/edit/command.js +1 -0
  41. package/dist/src/import/command.js +1 -6
  42. package/dist/src/import/orchestrator.d.ts +0 -1
  43. package/dist/src/import/orchestrator.js +18 -14
  44. package/dist/src/import/tui/WizardApp.d.ts +1 -2
  45. package/dist/src/import/tui/WizardApp.js +8 -8
  46. package/dist/src/import/tui/final-review-host.d.ts +1 -2
  47. package/dist/src/import/tui/final-review-host.js +2 -2
  48. package/dist/src/import/tui/runLivePreview.d.ts +0 -3
  49. package/dist/src/import/tui/runLivePreview.js +1 -4
  50. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -2
  51. package/dist/src/import/tui/steps/GenerateReviewStep.js +3 -3
  52. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +2 -6
  53. package/dist/src/import/tui/steps/WizardPreviewStep.js +3 -12
  54. package/dist/src/import/tui/useFinalizePreview.d.ts +0 -2
  55. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  56. package/dist/src/import/tui/useLivePreview.d.ts +0 -2
  57. package/dist/src/import/tui/useLivePreview.js +0 -1
  58. package/dist/src/index.js +6 -1
  59. package/dist/src/lib/command-options.d.ts +0 -1
  60. package/dist/src/lib/command-options.js +0 -3
  61. package/dist/src/print/command.js +16 -13
  62. package/dist/src/program.js +8 -1
  63. package/dist/src/runs/modify-launcher.d.ts +0 -2
  64. package/dist/src/runs/modify-launcher.js +0 -2
  65. package/dist/src/runs/push-helpers.d.ts +0 -1
  66. package/dist/src/runs/push-helpers.js +0 -3
  67. package/dist/src/runs/replay-helpers.d.ts +0 -4
  68. package/dist/src/runs/replay-helpers.js +0 -3
  69. package/package.json +6 -5
@@ -8,6 +8,7 @@ import { loadReviewInput } from './parser.js';
8
8
  import { App } from './tui/App.js';
9
9
  import { openPipelineDb, loadRawComponents, storeRawComponents, createStep, updateStep } from '../../session/db.js';
10
10
  import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
11
+ import { bindAnalyticsSessionId, enrichCommandResult, exitWithAnalytics } from '../../analytics/index.js';
11
12
  const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
12
13
  const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
13
14
  function applyDotPath(obj, path, value) {
@@ -102,6 +103,7 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
102
103
  }
103
104
  const accepted = snapshot.components.filter((c) => c.status === 'accepted').length;
104
105
  const rejected = snapshot.components.filter((c) => c.status === 'rejected').length + (names.length || 0);
106
+ enrichCommandResult({ accepted_component_count: accepted });
105
107
  process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected}\n`);
106
108
  return;
107
109
  }
@@ -130,7 +132,7 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
130
132
  lines.push('');
131
133
  lines.push('Re-run with --exclude-invalid to auto-reject these components, or run analyze select interactively to fix them.');
132
134
  process.stderr.write(lines.join('\n') + '\n');
133
- process.exit(1);
135
+ await exitWithAnalytics(1);
134
136
  return;
135
137
  }
136
138
  }
@@ -175,14 +177,14 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
175
177
  const parsed = JSON.parse(raw);
176
178
  if (!Array.isArray(parsed)) {
177
179
  process.stderr.write(`Error: --patch file must be a JSON array of patch operations: ${opts.patch}\n`);
178
- process.exit(1);
180
+ await exitWithAnalytics(1);
179
181
  return;
180
182
  }
181
183
  patchOps = parsed;
182
184
  }
183
185
  catch {
184
186
  process.stderr.write(`Error: cannot read or parse --patch file: ${opts.patch}\n`);
185
- process.exit(1);
187
+ await exitWithAnalytics(1);
186
188
  return;
187
189
  }
188
190
  // Warn on unknown component names
@@ -229,6 +231,7 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
229
231
  db.close();
230
232
  }
231
233
  }
234
+ enrichCommandResult({ accepted_component_count: accepted.length });
232
235
  process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
233
236
  }
234
237
  /**
@@ -345,7 +348,7 @@ export async function rejectComponentsByName(sessionId, names, opts = {}) {
345
348
  const components = snapshot.components.map((c) => (nameSet.has(c.name) ? { ...c, status: 'rejected' } : c));
346
349
  await saveReviewState(paths.statePath, { ...snapshot, components });
347
350
  }
348
- function resolveSessionId(sessionFlag) {
351
+ async function resolveSessionId(sessionFlag) {
349
352
  if (sessionFlag)
350
353
  return sessionFlag;
351
354
  const db = openPipelineDb();
@@ -360,7 +363,7 @@ function resolveSessionId(sessionFlag) {
360
363
  .get();
361
364
  if (!row) {
362
365
  process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
363
- process.exit(1);
366
+ return await exitWithAnalytics(1);
364
367
  }
365
368
  return row.id;
366
369
  }
@@ -384,7 +387,8 @@ export function registerAnalyzeEditCommand(program) {
384
387
  .option('--exclude-invalid', 'With --select-all: auto-reject components with validation errors instead of failing loud (opt-in bypass for the gate)')
385
388
  .option('--exclude-components <names>', 'Comma-separated component names to force-reject, regardless of other selection flags')
386
389
  .action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, excludeInvalid, excludeComponents, }) => {
387
- const sessionId = resolveSessionId(sessionFlag);
390
+ const sessionId = await resolveSessionId(sessionFlag);
391
+ await bindAnalyticsSessionId(sessionId);
388
392
  const db = openPipelineDb();
389
393
  let rawComponentCount = 0;
390
394
  try {
@@ -395,7 +399,7 @@ export function registerAnalyzeEditCommand(program) {
395
399
  }
396
400
  if (rawComponentCount === 0) {
397
401
  process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
398
- process.exit(1);
402
+ await exitWithAnalytics(1);
399
403
  return;
400
404
  }
401
405
  const artifactsRoot = getRefineArtifactsRoot();
@@ -420,7 +424,7 @@ export function registerAnalyzeEditCommand(program) {
420
424
  }
421
425
  catch (error) {
422
426
  process.stderr.write(`Error: unable to initialize refine session.\n${error instanceof Error ? error.message : String(error)}\n`);
423
- process.exit(1);
427
+ await exitWithAnalytics(1);
424
428
  return;
425
429
  }
426
430
  // Non-interactive path
@@ -465,7 +469,7 @@ export function registerAnalyzeEditCommand(program) {
465
469
  });
466
470
  if (process.stdout.columns !== undefined && process.stdout.columns < 60) {
467
471
  process.stderr.write(`Error: terminal too narrow (${process.stdout.columns} cols). Resize to 60+ columns.\n`);
468
- process.exit(1);
472
+ await exitWithAnalytics(1);
469
473
  }
470
474
  const { waitUntilExit } = render(createElement(App, {
471
475
  sessionId,
@@ -80,10 +80,7 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
80
80
  if (!manifest.componentsManifest)
81
81
  manifest.componentsManifest = {};
82
82
  const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
83
- // This preview is display-only (component-picker annotations); it never
84
- // triggers an apply, so always request the full diff to keep the
85
- // existing "removed" annotation working.
86
- const preview = await client.previewImport(manifest, true);
83
+ const preview = await client.previewImport(manifest);
87
84
  const annotations = {};
88
85
  for (const item of preview.components.new) {
89
86
  const name = item.name ?? '';
@@ -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,17 +45,17 @@ 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;
51
54
  private requestWithRetry;
52
55
  validateToken(): Promise<void>;
53
56
  checkPreflight(): Promise<void>;
54
- previewImport(manifest: ManifestPayload, allowDeletions?: boolean): Promise<ServerPreviewResponse>;
55
- applyImport(manifest: ManifestPayload, options: {
56
- acknowledgeBreakingChanges: boolean;
57
- allowDeletions?: boolean;
58
- }): Promise<ApplyOperationResponse>;
57
+ previewImport(manifest: ManifestPayload): Promise<ServerPreviewResponse>;
58
+ applyImport(manifest: ManifestPayload, acknowledgeBreakingChanges: boolean): Promise<ApplyOperationResponse>;
59
59
  pollOperation(operationId: string, opts?: {
60
60
  intervalMs?: number;
61
61
  maxIntervalMs?: number;
@@ -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)
@@ -307,20 +320,19 @@ export class ImportApiClient {
307
320
  return; // legacy backend without the endpoint
308
321
  throw new ApiError(`preflight failed: ${res.status}`, res.status, body);
309
322
  }
310
- async previewImport(manifest, allowDeletions = false) {
323
+ async previewImport(manifest) {
311
324
  const debug = getDebugLogger();
312
325
  const startedAt = Date.now();
313
326
  debug.event('apply', 'preview.request', {
314
327
  url: `${this.base()}/design_systems/imports/preview`,
315
328
  componentCount: manifest.components?.length ?? 0,
316
329
  tokenCount: manifest.designTokens?.length ?? 0,
317
- allowDeletions,
318
330
  });
319
331
  const result = await this.requestWithRetry('preview', PREVIEW_ERROR_PREFIX, () => designSystemImportSourcelessPreview({
320
332
  baseUrl: this.host,
321
333
  headers: this.headers(),
322
334
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
323
- body: { ...manifest, allowDeletions },
335
+ body: manifest,
324
336
  parseAs: 'json',
325
337
  }));
326
338
  if (!result.response.ok) {
@@ -336,14 +348,12 @@ export class ImportApiClient {
336
348
  debug.event('apply', 'preview.ok', { status: result.response.status, durationMs: Date.now() - startedAt });
337
349
  return sanitizePreviewResponse(parsed);
338
350
  }
339
- async applyImport(manifest, options) {
340
- const { acknowledgeBreakingChanges, allowDeletions = false } = options;
351
+ async applyImport(manifest, acknowledgeBreakingChanges) {
341
352
  const debug = getDebugLogger();
342
353
  const startedAt = Date.now();
343
354
  debug.event('apply', 'apply.request', {
344
355
  url: `${this.base()}/design_systems/imports/apply`,
345
356
  acknowledgeBreakingChanges,
346
- allowDeletions,
347
357
  });
348
358
  let result;
349
359
  try {
@@ -351,7 +361,7 @@ export class ImportApiClient {
351
361
  baseUrl: this.host,
352
362
  headers: this.headers(),
353
363
  path: { spaceId: this.spaceId, environmentId: this.environmentId },
354
- body: { ...manifest, acknowledgeBreakingChanges, allowDeletions },
364
+ body: { ...manifest, acknowledgeBreakingChanges },
355
365
  parseAs: 'json',
356
366
  });
357
367
  }
@@ -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<{