@contentful/experience-design-system-cli 2.2.1 → 2.5.1

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/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.2.1",
3
+ "version": "2.5.1",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "experience-design-system-cli": "./bin/cli.js",
8
+ "experiences": "./bin/cli.js",
8
9
  "exo": "./bin/cli.js"
9
10
  },
10
11
  "main": "./dist/src/index.js",
@@ -78,8 +78,6 @@ export function App({ sessionId, artifactsRoot, reviewRoot }) {
78
78
  if (!manifest.componentsManifest)
79
79
  manifest.componentsManifest = {};
80
80
  const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
81
- const orgId = await client.resolveOrganizationId();
82
- client.setOrganizationId(orgId);
83
81
  const preview = await client.previewImport(manifest);
84
82
  const annotations = {};
85
83
  for (const item of preview.components.new) {
@@ -5,7 +5,6 @@ export interface ApiClientOptions {
5
5
  cmaToken: string;
6
6
  spaceId: string;
7
7
  environmentId: string;
8
- organizationId?: string;
9
8
  }
10
9
  export declare class ApiError extends Error {
11
10
  readonly status: number;
@@ -17,13 +16,10 @@ export declare class ImportApiClient {
17
16
  private token;
18
17
  private spaceId;
19
18
  private environmentId;
20
- private organizationId;
21
19
  constructor(opts: ApiClientOptions);
22
- setOrganizationId(orgId: string | null): void;
23
20
  private base;
24
21
  private headers;
25
- resolveOrganizationId(): Promise<string | null>;
26
- validateEnvironment(): Promise<void>;
22
+ validateToken(): Promise<void>;
27
23
  previewImport(manifest: ManifestPayload): Promise<ServerPreviewResponse>;
28
24
  applyImport(manifest: ManifestPayload, acknowledgeBreakingChanges: boolean): Promise<ApplyOperationResponse>;
29
25
  pollOperation(operationId: string, opts?: {
@@ -19,8 +19,6 @@ async function request(url, options) {
19
19
  Authorization: `Bearer ${options.token}`,
20
20
  'Content-Type': 'application/json',
21
21
  };
22
- if (options.orgId)
23
- headers['x-contentful-organization-id'] = options.orgId;
24
22
  const init = {
25
23
  method: options.method ?? 'GET',
26
24
  headers,
@@ -34,58 +32,35 @@ export class ImportApiClient {
34
32
  token;
35
33
  spaceId;
36
34
  environmentId;
37
- organizationId;
38
35
  constructor(opts) {
39
36
  this.host = opts.host ?? DEFAULT_HOST;
40
37
  this.token = opts.cmaToken;
41
38
  this.spaceId = opts.spaceId;
42
39
  this.environmentId = opts.environmentId;
43
- this.organizationId = opts.organizationId;
44
- }
45
- setOrganizationId(orgId) {
46
- if (orgId)
47
- this.organizationId = orgId;
48
40
  }
49
41
  base() {
50
42
  return `${this.host}/spaces/${this.spaceId}/environments/${this.environmentId}`;
51
43
  }
52
44
  headers() {
53
- const h = {
45
+ return {
54
46
  Authorization: `Bearer ${this.token}`,
55
47
  'Content-Type': 'application/json',
56
48
  };
57
- if (this.organizationId)
58
- h['x-contentful-organization-id'] = this.organizationId;
59
- return h;
60
- }
61
- async resolveOrganizationId() {
62
- const url = `${this.host}/spaces/${this.spaceId}`;
63
- const res = await request(url, { token: this.token });
64
- if (res.status === 401 || res.status === 403) {
65
- throw new ApiError(`space '${this.spaceId}' not found or CMA token lacks access`, res.status, await res.text());
66
- }
67
- if (res.status === 404) {
68
- // Custom host (e.g. staging) may not expose the spaces API or the space may not
69
- // exist there. Skip the org ID — the design systems API may not require it.
70
- return null;
71
- }
72
- if (!res.ok) {
73
- throw new ApiError(`unexpected error fetching space: ${res.status}`, res.status, await res.text());
74
- }
75
- const json = (await res.json());
76
- return json.sys.organization.sys.id;
77
49
  }
78
- async validateEnvironment() {
79
- const url = `${this.host}/spaces/${this.spaceId}/environments/${this.environmentId}`;
50
+ async validateToken() {
51
+ // /users/me is the canonical "is this token valid" endpoint — it doesn't enforce
52
+ // space-membership rules that don't apply to the design-systems API path, so it
53
+ // doesn't false-positive 401 for tokens that are entitled to call import endpoints
54
+ // but lack the role assignments public CMA's SpacesController/EnvironmentsController
55
+ // require. Per-space/per-org authorization is enforced by the design-systems API itself
56
+ // on the actual preview/apply call.
57
+ const url = `${this.host}/users/me`;
80
58
  const res = await request(url, { token: this.token });
81
- if (res.status === 404) {
82
- throw new ApiError(`environment '${this.environmentId}' not found in space '${this.spaceId}'`, 404, await res.text());
83
- }
84
- if (res.status === 401 || res.status === 403) {
85
- throw new ApiError(`space '${this.spaceId}' not found or CMA token lacks access`, res.status, await res.text());
59
+ if (res.status === 401) {
60
+ throw new ApiError('CMA token is invalid or revoked', res.status, await res.text());
86
61
  }
87
62
  if (!res.ok) {
88
- throw new ApiError(`unexpected error validating environment: ${res.status}`, res.status, await res.text());
63
+ throw new ApiError(`unexpected error validating token: ${res.status}`, res.status, await res.text());
89
64
  }
90
65
  }
91
66
  async previewImport(manifest) {
@@ -392,9 +392,7 @@ export function registerApplyCommand(program) {
392
392
  }
393
393
  const { components, tokens, client } = inputs;
394
394
  try {
395
- const orgId = await client.resolveOrganizationId();
396
- client.setOrganizationId(orgId);
397
- await client.validateEnvironment();
395
+ await client.validateToken();
398
396
  }
399
397
  catch (e) {
400
398
  if (e instanceof ApiError)
@@ -459,9 +457,7 @@ export function registerApplyCommand(program) {
459
457
  }
460
458
  const { components, tokens, client } = inputs;
461
459
  try {
462
- const orgId = await client.resolveOrganizationId();
463
- client.setOrganizationId(orgId);
464
- await client.validateEnvironment();
460
+ await client.validateToken();
465
461
  }
466
462
  catch (e) {
467
463
  if (e instanceof ApiError)
@@ -638,9 +634,7 @@ export function registerApplyCommand(program) {
638
634
  }
639
635
  const { components, tokens, client } = inputs;
640
636
  try {
641
- const orgId = await client.resolveOrganizationId();
642
- client.setOrganizationId(orgId);
643
- await client.validateEnvironment();
637
+ await client.validateToken();
644
638
  }
645
639
  catch (e) {
646
640
  if (e instanceof ApiError)
@@ -15,7 +15,7 @@ export function ServerPreviewView({ preview, spaceId, environmentId }) {
15
15
  const { components, tokens } = preview;
16
16
  const totalComponents = components.new.length + components.changed.length + components.unchanged.length + components.removed.length;
17
17
  const totalTokens = tokens.new.length + tokens.changed.length + tokens.unchanged.length + tokens.removed.length;
18
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Preview \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), totalComponents > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Component Types (", totalComponents, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2726 ", components.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", components.changed.length, " to update"] }), _jsxs(Text, { color: "red", children: [" \u2717 ", components.removed.length, " to remove"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", components.unchanged.length, " unchanged"] }), components.changed.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: item.hasPendingDraftChanges })] }), _jsx(BreakingBadge, { item: item })] }, i))), _jsx(Text, { children: " " })] })), totalTokens > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Design Tokens (", totalTokens, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2726 ", tokens.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", tokens.changed.length, " to update"] }), _jsxs(Text, { color: "red", children: [" \u2717 ", tokens.removed.length, " to remove"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", tokens.unchanged.length, " unchanged"] }), tokens.changed
18
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Preview \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), totalComponents > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Component Types (", totalComponents, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2746 ", components.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", components.changed.length, " to update"] }), _jsxs(Text, { color: "red", children: [" \u2717 ", components.removed.length, " to remove"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", components.unchanged.length, " unchanged"] }), components.changed.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: item.hasPendingDraftChanges })] }), _jsx(BreakingBadge, { item: item })] }, i))), _jsx(Text, { children: " " })] })), totalTokens > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [" Design Tokens (", totalTokens, " total)"] }), _jsxs(Text, { color: "green", children: [" \u2746 ", tokens.new.length, " to create"] }), _jsxs(Text, { color: "yellow", children: [" ~ ", tokens.changed.length, " to update"] }), _jsxs(Text, { color: "red", children: [" \u2717 ", tokens.removed.length, " to remove"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", tokens.unchanged.length, " unchanged"] }), tokens.changed
19
19
  .filter((t) => t.hasPendingDraftChanges)
20
20
  .map((item, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: "yellow", children: [" ~ ", item.current.name] }), _jsx(DraftWarning, { hasDraft: true })] }, i))), _jsx(Text, { children: " " })] })), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
21
21
  }
@@ -1,8 +1,8 @@
1
- export type ExoCredentials = {
1
+ export type ExperiencesCredentials = {
2
2
  spaceId: string;
3
3
  environmentId: string;
4
4
  cmaToken: string;
5
5
  };
6
- export declare function readExoCredentials(): Promise<ExoCredentials>;
7
- export declare function writeExoCredentials(creds: ExoCredentials): Promise<void>;
8
- export declare function exoCredentialsPath(): string;
6
+ export declare function readExperiencesCredentials(): Promise<ExperiencesCredentials>;
7
+ export declare function writeExperiencesCredentials(creds: ExperiencesCredentials): Promise<void>;
8
+ export declare function experiencesCredentialsPath(): string;
@@ -1,9 +1,9 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- const CREDENTIALS_DIR = join(homedir(), '.config', 'exo');
4
+ const CREDENTIALS_DIR = join(homedir(), '.config', 'experiences');
5
5
  const CREDENTIALS_PATH = join(CREDENTIALS_DIR, 'credentials.json');
6
- export async function readExoCredentials() {
6
+ export async function readExperiencesCredentials() {
7
7
  try {
8
8
  const raw = await readFile(CREDENTIALS_PATH, 'utf8');
9
9
  const parsed = JSON.parse(raw);
@@ -21,10 +21,10 @@ export async function readExoCredentials() {
21
21
  };
22
22
  }
23
23
  }
24
- export async function writeExoCredentials(creds) {
24
+ export async function writeExperiencesCredentials(creds) {
25
25
  await mkdir(CREDENTIALS_DIR, { recursive: true });
26
26
  await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
27
27
  }
28
- export function exoCredentialsPath() {
28
+ export function experiencesCredentialsPath() {
29
29
  return CREDENTIALS_PATH;
30
30
  }
@@ -1,6 +1,6 @@
1
1
  import { resolve, join } from 'node:path';
2
2
  import { runPipeline } from './orchestrator.js';
3
- import { readExoCredentials } from '../credentials-store.js';
3
+ import { readExperiencesCredentials } from '../credentials-store.js';
4
4
  export function registerImportCommand(program) {
5
5
  program
6
6
  .command('import')
@@ -40,7 +40,7 @@ export function registerImportCommand(program) {
40
40
  const { render } = await import('ink');
41
41
  const { createElement } = await import('react');
42
42
  const { WizardApp } = await import('./tui/WizardApp.js');
43
- const creds = await readExoCredentials();
43
+ const creds = await readExperiencesCredentials();
44
44
  const { waitUntilExit } = render(createElement(WizardApp, {
45
45
  initialSpaceId: creds.spaceId,
46
46
  initialEnvironmentId: creds.environmentId || 'master',
@@ -34,7 +34,7 @@ function runCli(args) {
34
34
  });
35
35
  });
36
36
  }
37
- const WIZARD_LOG = join(tmpdir(), 'exo-import-wizard.log');
37
+ const WIZARD_LOG = join(tmpdir(), 'experiences-import-wizard.log');
38
38
  function logStep(entry) {
39
39
  const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n';
40
40
  appendFileSync(WIZARD_LOG, line);
@@ -45,7 +45,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
45
45
  const terminalWidth = stdout?.columns ?? 80;
46
46
  const logInit = useRef(false);
47
47
  if (!logInit.current) {
48
- writeFileSync(WIZARD_LOG, `--- exo import session ${new Date().toISOString()} ---\n`);
48
+ writeFileSync(WIZARD_LOG, `--- experiences import session ${new Date().toISOString()} ---\n`);
49
49
  logInit.current = true;
50
50
  }
51
51
  const credentialsRef = useRef(null);
@@ -85,6 +85,8 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
85
85
  },
86
86
  errorStep: '',
87
87
  errorMessage: '',
88
+ errorAllowCredentialRetry: false,
89
+ authCheckStepNumber: 1,
88
90
  });
89
91
  useEffect(() => {
90
92
  sessionRef.current = {
@@ -139,15 +141,16 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
139
141
  logStep({ update: sanitized });
140
142
  setState((prev) => ({ ...prev, ...partial }));
141
143
  };
142
- // ── Agent auth pre-flight ───────────────────────────────────────────────────
144
+ // ── Agent auth pre-flight ───────────────────────────────────────────────────────
143
145
  const runAgentAuthCheck = async (nextStep) => {
144
- update({ step: 'checking-claude-auth' });
146
+ const authCheckStepNumber = nextStep === 'generating-tokens' ? 1 : state.tokensPath ? 4 : 3;
147
+ update({ step: 'checking-claude-auth', authCheckStepNumber });
145
148
  const status = await checkAgentAuth(state.agent);
146
149
  if (status === 'not-found') {
147
150
  update({
148
151
  step: 'error',
149
152
  errorStep: `${state.agent} auth check`,
150
- errorMessage: `The \`${state.agent}\` CLI was not found on your PATH.\n\nInstall it, then re-run \`exo import\`.`,
153
+ errorMessage: `The \`${state.agent}\` CLI was not found on your PATH.\n\nInstall it, then re-run \`experiences import\`.`,
151
154
  });
152
155
  return false;
153
156
  }
@@ -156,7 +159,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
156
159
  step: 'error',
157
160
  errorStep: `${state.agent} auth check`,
158
161
  errorMessage: `${state.agent} is not authenticated.\n\n` +
159
- `Run \`${state.agent}\` in your terminal to log in, then re-run \`exo import\`.\n\n` +
162
+ `Run \`${state.agent}\` in your terminal to log in, then re-run \`experiences import\`.\n\n` +
160
163
  'If you are using AWS Bedrock, run:\n' +
161
164
  ' aws sso login --profile <your-profile>',
162
165
  });
@@ -165,7 +168,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
165
168
  update({ step: nextStep });
166
169
  return true;
167
170
  };
168
- // ── Step runners ──────────────────────────────────────────────────────────
171
+ // ── Step runners ────────────────────────────────────────────────────────
169
172
  const runGenerateTokens = async (rawTokensPath, outDir) => {
170
173
  const result = await new Promise((res) => {
171
174
  const child = spawn('node', [
@@ -438,7 +441,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
438
441
  update({ step: 'validating-credentials' });
439
442
  try {
440
443
  const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
441
- await client.resolveOrganizationId();
444
+ await client.validateToken();
442
445
  const { extractSessionId, tokensPath } = sessionRef.current;
443
446
  void runPreview(extractSessionId, tokensPath, spaceId, environmentId, cmaToken);
444
447
  }
@@ -448,15 +451,18 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
448
451
  return;
449
452
  }
450
453
  const msg = e instanceof Error ? e.message : 'Credential check failed';
451
- update({ step: 'credentials', credentialsError: msg });
454
+ update({
455
+ step: 'error',
456
+ errorStep: 'validating-credentials',
457
+ errorMessage: msg,
458
+ errorAllowCredentialRetry: false,
459
+ });
452
460
  }
453
461
  };
454
462
  const runPreview = async (extractSessionId, tokensPath, spaceId, environmentId, cmaToken) => {
455
463
  update({ step: 'previewing' });
456
464
  try {
457
465
  const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
458
- const orgId = await client.resolveOrganizationId();
459
- client.setOrganizationId(orgId);
460
466
  let components = [];
461
467
  if (extractSessionId) {
462
468
  const db = openPipelineDb();
@@ -520,21 +526,25 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
520
526
  // Space-level config errors (e.g. "Design system public CMA is disabled") cannot be
521
527
  // fixed by re-entering credentials — send to error screen.
522
528
  if (bodyMsg && /disabled/i.test(bodyMsg)) {
523
- update({ step: 'error', errorStep: 'apply preview', errorMessage: `Preview failed: ${bodyMsg}` });
529
+ update({
530
+ step: 'error',
531
+ errorStep: 'apply preview',
532
+ errorMessage: `Preview failed: ${bodyMsg}`,
533
+ errorAllowCredentialRetry: false,
534
+ });
524
535
  return;
525
536
  }
526
537
  update({ step: 'credentials', credentialsError: e.message });
527
538
  return;
528
539
  }
529
540
  if (e.status === 404) {
530
- // 404 can come from two places:
531
- // - resolveOrganizationId: space not found on this host (common when using --host
532
- // with a staging/preview host but prod space IDs)
533
- // - previewImport: design systems endpoint doesn't exist for this space/environment
534
- // Neither is a credentials problem — show a clear error instead of looping.
541
+ // 404 from previewImport means the design systems endpoint doesn't exist for this
542
+ // space/environment (typically wrong --host or wrong space/env). Not a credentials
543
+ // problem — show a clear error instead of looping.
535
544
  update({
536
545
  step: 'error',
537
546
  errorStep: 'apply preview',
547
+ errorAllowCredentialRetry: true,
538
548
  errorMessage: `Not found (404). Check that the space ID, environment ID, and host are correct.\n\n` +
539
549
  ` Space: ${spaceId}\n` +
540
550
  ` Environment: ${environmentId}\n` +
@@ -543,11 +553,11 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
543
553
  });
544
554
  return;
545
555
  }
546
- update({ step: 'error', errorStep: 'apply preview', errorMessage: e.message });
556
+ update({ step: 'error', errorStep: 'apply preview', errorMessage: e.message, errorAllowCredentialRetry: true });
547
557
  return;
548
558
  }
549
559
  const msg = e instanceof Error ? e.message : 'Preview failed';
550
- update({ step: 'error', errorStep: 'apply preview', errorMessage: msg });
560
+ update({ step: 'error', errorStep: 'apply preview', errorMessage: msg, errorAllowCredentialRetry: true });
551
561
  }
552
562
  };
553
563
  const runPush = async (manifest, spaceId, environmentId, cmaToken, acknowledgeBreakingChanges, preview) => {
@@ -570,8 +580,6 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
570
580
  update({ step: 'pushing' });
571
581
  try {
572
582
  const client = new ImportApiClient({ cmaToken, spaceId, environmentId, host: apiHost });
573
- const orgId = await client.resolveOrganizationId();
574
- client.setOrganizationId(orgId);
575
583
  let operation = await client.applyImport(manifest, acknowledgeBreakingChanges);
576
584
  try {
577
585
  logStep({
@@ -650,7 +658,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
650
658
  }
651
659
  catch (e) {
652
660
  const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : 'Push failed';
653
- update({ step: 'error', errorStep: 'apply push', errorMessage: msg });
661
+ update({ step: 'error', errorStep: 'apply push', errorMessage: msg, errorAllowCredentialRetry: true });
654
662
  }
655
663
  };
656
664
  const runPrintFiles = async (extractSessionId, outDir) => {
@@ -667,7 +675,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
667
675
  // tokensPath is already on disk from generate-tokens step; just record it
668
676
  update({ step: 'print-gate', componentsPath });
669
677
  };
670
- // ── Effect: kick off automatic steps ─────────────────────────────────────
678
+ // ── Effect: kick off automatic steps ───────────────────────────────────────────────
671
679
  const tokenReuseChecked = useRef(false);
672
680
  useEffect(() => {
673
681
  if (state.step === 'generating-tokens') {
@@ -694,7 +702,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
694
702
  })();
695
703
  }
696
704
  }, [state.step]); // intentional: only re-run when step changes
697
- // ── Render ────────────────────────────────────────────────────────────────
705
+ // ── Render ────────────────────────────────────────────────────────────────────────────
698
706
  const noQuitSteps = [
699
707
  'checking-claude-auth',
700
708
  'validating-credentials',
@@ -734,7 +742,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
734
742
  }
735
743
  }, onQuit: () => process.exit(0) }));
736
744
  case 'checking-claude-auth':
737
- return (_jsx(RunningStep, { stepNumber: 1, totalSteps: totalSteps, title: `Checking ${state.agent}`, description: `Verifying ${state.agent} is installed and authenticated...` }));
745
+ return (_jsx(RunningStep, { stepNumber: state.authCheckStepNumber, totalSteps: totalSteps, title: `Checking ${state.agent}`, description: `Verifying ${state.agent} is installed and authenticated...` }));
738
746
  case 'generating-tokens':
739
747
  return (_jsx(RunningStep, { stepNumber: 1, totalSteps: totalSteps, title: "Generating token definitions", description: `${state.agent} is mapping your design tokens to DTCG format and writing tokens.json. This may take a few minutes.` }));
740
748
  case 'path-validation':
@@ -836,7 +844,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
836
844
  void runPreview(extractSessionId, tokensPath, state.spaceId, state.environmentId, state.cmaToken);
837
845
  }, onQuit: () => process.exit(0) }));
838
846
  case 'validating-credentials':
839
- return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Validating credentials", description: "Checking that your space ID and CMA token are valid..." }));
847
+ return (_jsx(RunningStep, { stepNumber: totalSteps - 1, totalSteps: totalSteps, title: "Validating credentials", description: "Checking that your space ID and CMA token are valid..." }));
840
848
  case 'previewing':
841
849
  return (_jsx(RunningStep, { stepNumber: totalSteps, totalSteps: totalSteps, title: "Computing diff", description: "Computing diff against your Contentful space..." }));
842
850
  case 'preview-gate':
@@ -857,13 +865,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
857
865
  hasTokens && state.tokensPath ? `tokens.json → ${state.tokensPath}` : null,
858
866
  ]
859
867
  .filter(Boolean)
860
- .join('\n'), context: "Your files are saved to disk. Run `exo import` again when you're ready to push to Contentful.", continueLabel: "Exit", showSkip: false, onContinue: () => process.exit(0), onQuit: () => process.exit(0) }));
868
+ .join('\n'), context: "Your files are saved to disk. Run `experiences import` again when you're ready to push to Contentful.", continueLabel: "Exit", showSkip: false, onContinue: () => process.exit(0), onQuit: () => process.exit(0) }));
861
869
  case 'done': {
862
870
  const totalFailed = state.pushResult.componentTypes.failed + state.pushResult.designTokens.failed;
863
871
  return (_jsx(DoneStep, { componentTypes: state.pushResult.componentTypes, designTokens: state.pushResult.designTokens, summary: state.pushResult.summary, spaceId: state.spaceId, environmentId: state.environmentId, onExit: () => process.exit(totalFailed > 0 ? 1 : 0) }));
864
872
  }
865
873
  case 'error':
866
- return _jsx(ErrorStep, { stepName: state.errorStep, message: state.errorMessage, onExit: () => process.exit(1) });
874
+ return (_jsx(ErrorStep, { stepName: state.errorStep, message: state.errorMessage, onExit: () => process.exit(1), onRetryCredentials: state.errorAllowCredentialRetry ? () => update({ step: 'credentials', credentialsError: '' }) : undefined }));
867
875
  }
868
876
  })();
869
877
  return (_jsxs(Box, { flexDirection: "column", width: terminalWidth, children: [_jsx(TopBar, { subcommand: "import", hints: hints }), stepContent] }));
@@ -74,6 +74,6 @@ export function CredentialsStep({ summary, error: externalError, initialSpaceId
74
74
  }
75
75
  const displayError = inlineError ?? externalError ?? null;
76
76
  return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [summary && _jsxs(Text, { color: "green", children: ["\u2713 ", summary] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: initialSpaceId && initialCmaToken
77
- ? 'Credentials pre-filled from exo setup. Press Enter to continue or edit any field to update.'
78
- : 'Enter your Contentful credentials to continue.' }) }), !(initialSpaceId && initialCmaToken) && (_jsx(Text, { dimColor: true, children: "Tip: run exo setup to save these to ~/.config/exo/credentials.json so they pre-fill here automatically." })), _jsxs(Box, { flexDirection: "column", gap: 0, marginTop: 1, children: [renderField('Space ID', spaceId, 'spaceId'), renderField('Environment', environmentId, 'environmentId'), renderField('CMA Token', cmaToken, 'cmaToken', true)] }), displayError && _jsxs(Text, { color: "red", children: ["\u2717 ", displayError] }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Next field / Submit" }), _jsx(Text, { dimColor: true, children: "[Tab] Switch field" }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
77
+ ? 'Credentials pre-filled from experiences setup. Press Enter to continue or edit any field to update.'
78
+ : 'Enter your Contentful credentials to continue.' }) }), !(initialSpaceId && initialCmaToken) && (_jsx(Text, { dimColor: true, children: "Tip: run experiences setup to save these to ~/.config/experiences/credentials.json so they pre-fill here automatically." })), _jsxs(Box, { flexDirection: "column", gap: 0, marginTop: 1, children: [renderField('Space ID', spaceId, 'spaceId'), renderField('Environment', environmentId, 'environmentId'), renderField('CMA Token', cmaToken, 'cmaToken', true)] }), displayError && _jsxs(Text, { color: "red", children: ["\u2717 ", displayError] }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter] Next field / Submit" }), _jsx(Text, { dimColor: true, children: "[Tab] Switch field" }), _jsx(Text, { dimColor: true, children: "[q] Quit" })] })] }));
79
79
  }
@@ -3,6 +3,7 @@ type ErrorStepProps = {
3
3
  stepName: string;
4
4
  message: string;
5
5
  onExit: () => void;
6
+ onRetryCredentials?: () => void;
6
7
  };
7
- export declare function ErrorStep({ stepName, message, onExit }: ErrorStepProps): React.ReactElement;
8
+ export declare function ErrorStep({ stepName, message, onExit, onRetryCredentials }: ErrorStepProps): React.ReactElement;
8
9
  export {};
@@ -1,11 +1,15 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
4
- export function ErrorStep({ stepName, message, onExit }) {
4
+ export function ErrorStep({ stepName, message, onExit, onRetryCredentials }) {
5
5
  useImmediateInput((input, key) => {
6
+ if (input === 'r' && onRetryCredentials) {
7
+ onRetryCredentials();
8
+ return;
9
+ }
6
10
  if (key.return || input === 'q' || key.escape) {
7
11
  onExit();
8
12
  }
9
13
  });
10
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, color: "red", children: ["\u2717 ", stepName, " failed"] }), _jsx(Text, { color: "red", children: message }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }) })] }));
14
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, color: "red", children: ["\u2717 ", stepName, " failed"] }), _jsx(Text, { color: "red", children: message }), _jsxs(Box, { gap: 3, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }), onRetryCredentials && _jsx(Text, { dimColor: true, children: "[r] Re-enter credentials" })] })] }));
11
15
  }
@@ -83,7 +83,7 @@ export declare function seedCDFFromPreviewResponse(db: DatabaseSync, sessionId:
83
83
  */
84
84
  export declare function seedDefaultsFromChangedItems(db: DatabaseSync, sessionId: string, changedItems: Array<{
85
85
  current: ComponentTypeSummary;
86
- proposed: Record<string, unknown>;
86
+ proposed: object;
87
87
  }>): number;
88
88
  /**
89
89
  * Ensures all props on generated components have a cdf_type.
@@ -5,7 +5,7 @@ import { homedir } from 'node:os';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { promisify } from 'node:util';
8
- import { readExoCredentials, writeExoCredentials, exoCredentialsPath } from '../credentials-store.js';
8
+ import { readExperiencesCredentials, writeExperiencesCredentials, experiencesCredentialsPath, } from '../credentials-store.js';
9
9
  const execFileAsync = promisify(execFile);
10
10
  const REQUIRED_NODE_MAJOR = 24;
11
11
  // ── Output helpers ────────────────────────────────────────────────────────────
@@ -169,7 +169,7 @@ async function setupNode() {
169
169
  info(`nvm detected. Will run: nvm install ${REQUIRED_NODE_MAJOR} && nvm use ${REQUIRED_NODE_MAJOR}`);
170
170
  const go = await confirm(`Install and switch to Node ${REQUIRED_NODE_MAJOR} via nvm?`);
171
171
  if (!go) {
172
- warn(`Skipped. Re-run exo setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
172
+ warn(`Skipped. Re-run experiences setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
173
173
  return false;
174
174
  }
175
175
  // nvm is a shell function so we source it and run in a subshell
@@ -184,14 +184,14 @@ async function setupNode() {
184
184
  info(`Run manually: nvm install ${REQUIRED_NODE_MAJOR} && nvm use ${REQUIRED_NODE_MAJOR}`);
185
185
  return false;
186
186
  }
187
- ok(`Node ${REQUIRED_NODE_MAJOR} installed via nvm. Re-run exo setup in a fresh shell to pick it up.`);
187
+ ok(`Node ${REQUIRED_NODE_MAJOR} installed via nvm. Re-run experiences setup in a fresh shell to pick it up.`);
188
188
  return false; // Need fresh shell to get the new node on PATH
189
189
  }
190
190
  if (hasFnm) {
191
191
  info(`fnm detected. Will run: fnm install ${REQUIRED_NODE_MAJOR} && fnm use ${REQUIRED_NODE_MAJOR}`);
192
192
  const go = await confirm(`Install and switch to Node ${REQUIRED_NODE_MAJOR} via fnm?`);
193
193
  if (!go) {
194
- warn(`Skipped. Re-run exo setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
194
+ warn(`Skipped. Re-run experiences setup after switching to Node ${REQUIRED_NODE_MAJOR}.`);
195
195
  return false;
196
196
  }
197
197
  const result = await runSpawn('fnm', ['install', String(REQUIRED_NODE_MAJOR)]);
@@ -212,7 +212,7 @@ async function setupNode() {
212
212
  info(`Run manually: fnm default ${REQUIRED_NODE_MAJOR}`);
213
213
  }
214
214
  }
215
- ok(`Node ${REQUIRED_NODE_MAJOR} installed via fnm. Re-run exo setup in a fresh shell.`);
215
+ ok(`Node ${REQUIRED_NODE_MAJOR} installed via fnm. Re-run experiences setup in a fresh shell.`);
216
216
  return false;
217
217
  }
218
218
  // No version manager found — offer to install nvm
@@ -229,7 +229,7 @@ async function setupNode() {
229
229
  info('Install manually: https://github.com/nvm-sh/nvm#installing-and-updating');
230
230
  return false;
231
231
  }
232
- ok('nvm installed. Open a new shell, then re-run exo setup.');
232
+ ok('nvm installed. Open a new shell, then re-run experiences setup.');
233
233
  return false;
234
234
  }
235
235
  info(`Install Node ${REQUIRED_NODE_MAJOR} manually from https://nodejs.org`);
@@ -308,7 +308,7 @@ async function setupBuild(repoRoot) {
308
308
  // ── Step 4: agent CLI ─────────────────────────────────────────────────────────
309
309
  async function setupAgent() {
310
310
  section('Step 4: Coding agent (claude, codex, opencode, or cursor)', '[required]');
311
- info('exo import uses a coding agent to generate component definitions.');
311
+ info('experiences import uses a coding agent to generate component definitions.');
312
312
  info('');
313
313
  const agents = [
314
314
  { name: 'Claude Code', binary: 'claude', installHint: 'npm install -g @anthropic-ai/claude-code && claude login' },
@@ -375,15 +375,15 @@ async function setupAgent() {
375
375
  info('Run `opencode auth` to configure your provider.');
376
376
  return true;
377
377
  }
378
- warn('Skipped. Install a coding agent before running exo import.');
378
+ warn('Skipped. Install a coding agent before running experiences import.');
379
379
  return false;
380
380
  }
381
381
  // ── Step 5: Contentful credentials ───────────────────────────────────────────
382
382
  async function setupContentfulCredentials() {
383
383
  section('Step 5: Contentful credentials', '[optional]');
384
- info(`Saved to ${exoCredentialsPath()} — loaded automatically by exo import.`);
384
+ info(`Saved to ${experiencesCredentialsPath()} — loaded automatically by experiences import.`);
385
385
  info('');
386
- const stored = await readExoCredentials();
386
+ const stored = await readExperiencesCredentials();
387
387
  const currentSpace = stored.spaceId;
388
388
  const currentEnv = stored.environmentId;
389
389
  const currentToken = stored.cmaToken;
@@ -417,7 +417,7 @@ async function setupContentfulCredentials() {
417
417
  ok('Credentials already configured — no changes made');
418
418
  }
419
419
  else {
420
- warn('Skipped. exo import will prompt for credentials interactively.');
420
+ warn('Skipped. experiences import will prompt for credentials interactively.');
421
421
  }
422
422
  return true;
423
423
  }
@@ -434,15 +434,15 @@ async function setupContentfulCredentials() {
434
434
  warn('Space ID and CMA token are required. Skipped.');
435
435
  return false;
436
436
  }
437
- await writeExoCredentials({ spaceId, environmentId, cmaToken });
438
- ok(`Credentials saved to ${exoCredentialsPath()}`);
439
- info('Run exo import — the credentials step will be pre-filled automatically.');
437
+ await writeExperiencesCredentials({ spaceId, environmentId, cmaToken });
438
+ ok(`Credentials saved to ${experiencesCredentialsPath()}`);
439
+ info('Run experiences import — the credentials step will be pre-filled automatically.');
440
440
  return true;
441
441
  }
442
442
  // ── Step 6: Optional quality-of-life ─────────────────────────────────────────
443
443
  async function setupQoL(profilePath) {
444
444
  section('Step 6: Optional extras', '[optional]');
445
- info('These are not required for exo import but improve the experience.');
445
+ info('These are not required for experiences import but improve the experience.');
446
446
  info('');
447
447
  // 6a: EDS_EXTRACT_CONCURRENCY
448
448
  const hasConcurrency = await profileContains(profilePath, 'EDS_EXTRACT_CONCURRENCY');
@@ -451,7 +451,7 @@ async function setupQoL(profilePath) {
451
451
  info('Default is 4. Set higher (e.g. 8) on fast machines to speed up large codebases.');
452
452
  const setConcurrency = await confirm('Add EDS_EXTRACT_CONCURRENCY=8 to your profile?', false);
453
453
  if (setConcurrency) {
454
- await appendToProfile(profilePath, '# exo performance\nexport EDS_EXTRACT_CONCURRENCY=8');
454
+ await appendToProfile(profilePath, '# experiences performance\nexport EDS_EXTRACT_CONCURRENCY=8');
455
455
  ok(`EDS_EXTRACT_CONCURRENCY=8 written to ${profilePath}`);
456
456
  }
457
457
  else {
@@ -595,7 +595,7 @@ async function checkAgent() {
595
595
  }
596
596
  }
597
597
  warn('No coding agent found on PATH');
598
- info('The coding agent is required for the generate steps in exo import.');
598
+ info('The coding agent is required for the generate steps in experiences import.');
599
599
  info('Install one of:');
600
600
  info(' • Claude Code: npm install -g @anthropic-ai/claude-code');
601
601
  info(' • OpenAI Codex: npm install -g @openai/codex');
@@ -606,11 +606,11 @@ async function checkAgent() {
606
606
  export function registerSetupCommand(program) {
607
607
  program
608
608
  .command('doctor')
609
- .description('Check prerequisites so exo import runs without errors')
609
+ .description('Check prerequisites so experiences import runs without errors')
610
610
  .option('--skip-build', 'Skip the pnpm install + build step (useful if already built)')
611
611
  .option('--skip-agent', 'Skip the coding agent check')
612
612
  .action(async (opts) => {
613
- process.stderr.write('\x1b[1mexo doctor\x1b[0m — checking your environment\n');
613
+ process.stderr.write('\x1b[1mexperiences doctor\x1b[0m — checking your environment\n');
614
614
  const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
615
615
  const results = [];
616
616
  const nodeOk = await checkNode();
@@ -651,30 +651,30 @@ export function registerSetupCommand(program) {
651
651
  }
652
652
  }
653
653
  if (requiredFailed.length === 0 && failed.length === 0) {
654
- process.stderr.write('\n\x1b[32m\x1b[1m✓ All checks passed. You are ready to run: exo import\x1b[0m\n\n');
654
+ process.stderr.write('\n\x1b[32m\x1b[1m✓ All checks passed. You are ready to run: experiences import\x1b[0m\n\n');
655
655
  process.exit(0);
656
656
  }
657
657
  else if (requiredFailed.length === 0) {
658
658
  process.stderr.write('\n\x1b[33m\x1b[1m⚠ Required checks passed, but optional checks failed.\x1b[0m\n');
659
- process.stderr.write(' You can run \x1b[1mexo import\x1b[0m but the generate steps may fail without a coding agent.\n\n');
659
+ process.stderr.write(' You can run \x1b[1mexperiences import\x1b[0m but the generate steps may fail without a coding agent.\n\n');
660
660
  process.exit(0);
661
661
  }
662
662
  else {
663
663
  process.stderr.write(`\n\x1b[31m\x1b[1m✗ ${requiredFailed.length} required check${requiredFailed.length === 1 ? '' : 's'} failed.\x1b[0m\n`);
664
- process.stderr.write(' Fix the issues above, then re-run \x1b[1mexo doctor\x1b[0m.\n\n');
664
+ process.stderr.write(' Fix the issues above, then re-run \x1b[1mexperiences doctor\x1b[0m.\n\n');
665
665
  process.exit(1);
666
666
  }
667
667
  });
668
668
  program
669
669
  .command('setup')
670
- .description('Interactive setup wizard: installs prerequisites and configures credentials for exo import')
670
+ .description('Interactive setup wizard: installs prerequisites and configures credentials for experiences import')
671
671
  .option('--skip-build', 'Skip the pnpm install + build step')
672
672
  .option('--skip-agent', 'Skip the coding agent check')
673
673
  .option('--skip-credentials', 'Skip the Contentful credentials step')
674
674
  .option('--skip-optional', 'Skip optional quality-of-life extras')
675
675
  .action(async (opts) => {
676
- process.stdout.write('\n\x1b[1mexo setup\x1b[0m — interactive setup wizard\n');
677
- process.stdout.write('Sets up everything you need to run \x1b[1mexo import\x1b[0m.\n');
676
+ process.stdout.write('\n\x1b[1mexperiences setup\x1b[0m — interactive setup wizard\n');
677
+ process.stdout.write('Sets up everything you need to run \x1b[1mexperiences import\x1b[0m.\n');
678
678
  process.stdout.write('Required steps are marked \x1b[31m[required]\x1b[0m, optional ones \x1b[2m[optional]\x1b[0m.\n');
679
679
  const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
680
680
  const repoRoot = join(pkgRoot, '..', '..');
@@ -684,7 +684,7 @@ export function registerSetupCommand(program) {
684
684
  const nodeOk = await setupNode();
685
685
  results.push({ name: 'Node.js 24+', passed: nodeOk, required: true });
686
686
  if (!nodeOk) {
687
- process.stdout.write('\n\x1b[33mNode.js setup requires a shell restart. Re-run exo setup afterwards.\x1b[0m\n\n');
687
+ process.stdout.write('\n\x1b[33mNode.js setup requires a shell restart. Re-run experiences setup afterwards.\x1b[0m\n\n');
688
688
  process.exit(0);
689
689
  }
690
690
  // Step 2: pnpm
@@ -736,19 +736,19 @@ export function registerSetupCommand(program) {
736
736
  }
737
737
  process.stdout.write('\n');
738
738
  if (requiredFailed.length === 0) {
739
- process.stdout.write('\x1b[32m\x1b[1m✓ Setup complete. You can now run: exo import\x1b[0m\n');
739
+ process.stdout.write('\x1b[32m\x1b[1m✓ Setup complete. You can now run: experiences import\x1b[0m\n');
740
740
  if (optionalFailed.length > 0) {
741
741
  process.stdout.write(" (Some optional steps were skipped — that's fine.)\n");
742
742
  }
743
743
  }
744
744
  else {
745
745
  process.stdout.write(`\x1b[33m\x1b[1m⚠ ${requiredFailed.length} required step${requiredFailed.length === 1 ? '' : 's'} incomplete.\x1b[0m\n`);
746
- process.stdout.write(' Complete the steps above, then re-run \x1b[1mexo setup\x1b[0m.\n');
746
+ process.stdout.write(' Complete the steps above, then re-run \x1b[1mexperiences setup\x1b[0m.\n');
747
747
  }
748
- // ── Offer exo doctor ───────────────────────────────────────────────────
748
+ // ── Offer experiences doctor ───────────────────────────────────────────────────
749
749
  process.stdout.write('\n');
750
750
  const runDoctor = process.stdout.isTTY &&
751
- (await confirm('Run exo doctor now to verify your environment?', requiredFailed.length === 0));
751
+ (await confirm('Run experiences doctor now to verify your environment?', requiredFailed.length === 0));
752
752
  if (runDoctor) {
753
753
  process.stdout.write('\n');
754
754
  const cliBin = process.argv[1] ?? fileURLToPath(import.meta.url);
@@ -759,7 +759,7 @@ export function registerSetupCommand(program) {
759
759
  process.stderr.write(doctorResult.stderr);
760
760
  process.exit(doctorResult.exitCode);
761
761
  }
762
- process.stdout.write('\nRun \x1b[1mexo doctor\x1b[0m at any time to re-check your environment.\n\n');
762
+ process.stdout.write('\nRun \x1b[1mexperiences doctor\x1b[0m at any time to re-check your environment.\n\n');
763
763
  process.exit(requiredFailed.length === 0 ? 0 : 1);
764
764
  });
765
765
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.2.1",
3
+ "version": "2.5.1",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "experience-design-system-cli": "./bin/cli.js",
8
+ "experiences": "./bin/cli.js",
8
9
  "exo": "./bin/cli.js"
9
10
  },
10
11
  "main": "./dist/src/index.js",
@@ -30,7 +31,7 @@
30
31
  "react-dom": "^18.3.1",
31
32
  "ts-morph": "^27.0.2",
32
33
  "typescript": "^5.9.3",
33
- "@contentful/experience-design-system-types": "2.2.1"
34
+ "@contentful/experience-design-system-types": "2.5.1"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@tsconfig/node24": "^24.0.3",